Repository: sjbarag/brs
Pull request #249 (2019-05-29 23:26:54)

fix(lex): Allow single-word #elseif and #endif
The two-word variants `#else if` and `#end if` were already supported, but I didn't realize the single-word versions were valid in RBI!  Now they're valid here too.


Patch:
diff --git a/package.json b/package.json
index 275226e..d1dbcd2 100644
--- a/package.json
+++ b/package.json
@@ -14,8 +14,8 @@
     "clean": "rimraf ./lib ./types",
     "test": "jest",
     "lint": "tslint --project .",
-    "prettier:write": "prettier --write '{bin,src,test}/**/*.{js,ts}'",
-    "prettier": "prettier --check '{bin,src,test}/**/*.{js,ts}'",
+    "prettier:write": "prettier --write \"{bin,src,test}/**/*.{js,ts}\"",
+    "prettier": "prettier --check \"{bin,src,test}/**/*.{js,ts}\"",
     "prepublishOnly": "npm-run-all --serial clean build lint prettier test"
   },
   "files": [
diff --git a/src/lexer/Lexeme.ts b/src/lexer/Lexeme.ts
index 4b7367a..2860d87 100644
--- a/src/lexer/Lexeme.ts
+++ b/src/lexer/Lexeme.ts
@@ -102,6 +102,7 @@ export enum Lexeme {
     Rem = "Rem",
     Return = "Return",
     Step = "Step",
+    Stop = "Stop",
     Sub = "Sub",
     Tab = "Tab",
     To = "To",
diff --git a/src/lexer/Lexer.ts b/src/lexer/Lexer.ts
index d61f28c..c823beb 100644
--- a/src/lexer/Lexer.ts
+++ b/src/lexer/Lexer.ts
@@ -694,6 +694,12 @@ export class Lexer {
                 case "#else":
                     addToken(Lexeme.HashElse);
                     return;
+                case "#elseif":
+                    addToken(Lexeme.HashElseIf);
+                    return;
+                case "#endif":
+                    addToken(Lexeme.HashEndIf);
+                    return;
                 case "#const":
                     addToken(Lexeme.HashConst);
                     return;
diff --git a/src/lexer/ReservedWords.ts b/src/lexer/ReservedWords.ts
index 891dc6d..ddbaa5a 100644
--- a/src/lexer/ReservedWords.ts
+++ b/src/lexer/ReservedWords.ts
@@ -91,6 +91,7 @@ export const KeyWords: { [key: string]: L } = {
     rem: L.Rem,
     return: L.Return,
     step: L.Step,
+    stop: L.Stop,
     sub: L.Sub,
     to: L.To,
     true: L.True,
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index 0e8e24d..a4ef3ad 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -78,6 +78,7 @@ const allowedProperties = [
     Lexeme.Rem,
     Lexeme.Return,
     Lexeme.Step,
+    Lexeme.Stop,
     Lexeme.Sub,
     Lexeme.Tab,
     Lexeme.To,
@@ -552,6 +553,10 @@ export class Parser {
                 return libraryStatement();
             }
 
+            if (check(Lexeme.Stop)) {
+                return stopStatement();
+            }
+
             if (check(Lexeme.If)) {
                 return ifStatement();
             }
@@ -800,7 +805,7 @@ export class Parser {
 
                 //keep track of the current error count, because if the then branch fails,
                 //we will trash them in favor of a single error on if
-                var errorsLengthBeforeBlock = errors.length;
+                let errorsLengthBeforeBlock = errors.length;
 
                 // we're parsing a multi-line ("block") form of the BrightScript if/then/else and must find
                 // a trailing "end if"
@@ -1056,7 +1061,14 @@ export class Parser {
                 | Expr.Expression
                 | Stmt.PrintSeparator.Tab
                 | Stmt.PrintSeparator.Space)[] = [];
-            values.push(expression());
+
+            //print statements can be empty, so look for empty print conditions
+            if (isAtEnd() || check(Lexeme.Newline, Lexeme.Colon)) {
+                let emptyStringLiteral = new Expr.Literal(new BrsString(""), printKeyword.location);
+                values.push(emptyStringLiteral);
+            } else {
+                values.push(expression());
+            }
 
             while (!check(Lexeme.Newline, Lexeme.Colon, ...additionalterminators) && !isAtEnd()) {
                 if (check(Lexeme.Semicolon)) {
@@ -1106,13 +1118,24 @@ export class Parser {
          * Parses an `end` statement
          * @returns an AST representation of an `end` statement.
          */
-        function endStatement(): Stmt.End {
+        function endStatement() {
             let tokens = { end: advance() };
 
             while (match(Lexeme.Newline));
 
             return new Stmt.End(tokens);
         }
+        /**
+         * Parses a `stop` statement
+         * @returns an AST representation of a `stop` statement
+         */
+        function stopStatement() {
+            let tokens = { stop: advance() };
+
+            while (match(Lexeme.Newline, Lexeme.Colon));
+
+            return new Stmt.Stop(tokens);
+        }
 
         /**
          * Parses a block, looking for a specific terminating Lexeme to denote completion.
diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts
index 9129a56..61822ae 100644
--- a/src/parser/Statement.ts
+++ b/src/parser/Statement.ts
@@ -293,6 +293,27 @@ export class End implements Statement {
     }
 }
 
+export class Stop implements Statement {
+    constructor(
+        readonly tokens: {
+            stop: Token;
+        }
+    ) {}
+
+    accept<R>(visitor: Visitor<R>): BrsType {
+        //TODO implement this in the runtime. It should pause code execution until a `c` command is issued from the console
+        throw new Error("Not implemented");
+    }
+
+    get location() {
+        return {
+            file: this.tokens.stop.location.file,
+            start: this.tokens.stop.location.start,
+            end: this.tokens.stop.location.end,
+        };
+    }
+}
+
 export class For implements Statement {
     constructor(
         readonly tokens: {


Test patch:
diff --git a/test/lexer/Lexer.test.js b/test/lexer/Lexer.test.js
index 09dde50..0770043 100644
--- a/test/lexer/Lexer.test.js
+++ b/test/lexer/Lexer.test.js
@@ -23,6 +23,11 @@ describe("lexer", () => {
         ]);
     });
 
+    it("gives the `stop` keyword its own Lexeme", () => {
+        let { tokens } = Lexer.scan("stop");
+        expect(tokens.map(t => t.kind)).toEqual([Lexeme.Stop, Lexeme.Eof]);
+    });
+
     it("aliases '?' to 'print'", () => {
         let { tokens } = Lexer.scan("?2");
         expect(tokens.map(t => t.kind)).toEqual([Lexeme.Print, Lexeme.Integer, Lexeme.Eof]);
@@ -336,12 +341,14 @@ describe("lexer", () => {
         });
 
         it("reads conditional directives", () => {
-            let { tokens } = Lexer.scan("#if #else if #else #end if");
+            let { tokens } = Lexer.scan("#if #else if #elseif #else #end if #endif");
             expect(tokens.map(t => t.kind)).toEqual([
                 Lexeme.HashIf,
                 Lexeme.HashElseIf,
+                Lexeme.HashElseIf,
                 Lexeme.HashElse,
                 Lexeme.HashEndIf,
+                Lexeme.HashEndIf,
                 Lexeme.Eof,
             ]);
         });
diff --git a/test/parser/statement/PrintStatement.test.js b/test/parser/statement/PrintStatement.test.js
index 5531f00..37aa43f 100644
--- a/test/parser/statement/PrintStatement.test.js
+++ b/test/parser/statement/PrintStatement.test.js
@@ -24,6 +24,14 @@ describe("parser print statements", () => {
         expect(statements).toMatchSnapshot();
     });
 
+    it("supports empty print", () => {
+        let { statements, errors } = brs.parser.Parser.parse([token(Lexeme.Print), EOF]);
+        expect(errors).toEqual([]);
+        expect(statements).toBeDefined();
+        expect(statements).not.toBeNull();
+        expect(statements).toMatchSnapshot();
+    });
+
     it("parses print lists with no separator", () => {
         let { statements, errors } = parser.parse([
             token(Lexeme.Print),
diff --git a/test/parser/statement/Stop.test.js b/test/parser/statement/Stop.test.js
new file mode 100644
index 0000000..18932c9
--- /dev/null
+++ b/test/parser/statement/Stop.test.js
@@ -0,0 +1,42 @@
+const brs = require("brs");
+const { Lexeme } = brs.lexer;
+const { Int32 } = brs.types;
+
+const { token, identifier, EOF } = require("../ParserTests");
+
+describe("stop statement", () => {
+    it("cannot be used as a local variable", () => {
+        let { statements, errors } = brs.parser.Parser.parse([
+            token(Lexeme.Stop, "stop"),
+            token(Lexeme.Equal, "="),
+            token(Lexeme.True, "true"),
+            EOF,
+        ]);
+
+        //should be an error
+        expect(errors).toHaveLength(1);
+        expect(statements).toBeDefined();
+        expect(statements).not.toBeNull();
+        expect(statements).toMatchSnapshot();
+    });
+
+    it("is valid as a statement", () => {
+        let { statements, errors } = brs.parser.Parser.parse([token(Lexeme.Stop, "stop"), EOF]);
+        expect(errors[0]).toBeUndefined();
+        expect(statements).toMatchSnapshot();
+    });
+
+    it("can be used as an object property", () => {
+        let { tokens } = brs.lexer.Lexer.scan(`
+            sub Main()
+                theObject = {
+                    stop: false
+                }
+                theObject.stop = true
+            end sub
+        `);
+        let { statements, errors } = brs.parser.Parser.parse(tokens);
+        expect(errors.length).toEqual(0);
+        expect(statements).toMatchSnapshot();
+    });
+});
diff --git a/test/parser/statement/__snapshots__/PrintStatement.test.js.snap b/test/parser/statement/__snapshots__/PrintStatement.test.js.snap
index 7272b59..bc83014 100644
--- a/test/parser/statement/__snapshots__/PrintStatement.test.js.snap
+++ b/test/parser/statement/__snapshots__/PrintStatement.test.js.snap
@@ -221,3 +221,46 @@ Array [
   },
 ]
 `;
+
+exports[`parser print statements supports empty print 1`] = `
+Array [
+  Print {
+    "expressions": Array [
+      Literal {
+        "_location": Object {
+          "end": Object {
+            "column": -9,
+            "line": -9,
+          },
+          "start": Object {
+            "column": -9,
+            "line": -9,
+          },
+        },
+        "value": BrsString {
+          "kind": 2,
+          "value": "",
+        },
+      },
+    ],
+    "tokens": Object {
+      "print": Object {
+        "isReserved": false,
+        "kind": "Print",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": -9,
+            "line": -9,
+          },
+          "start": Object {
+            "column": -9,
+            "line": -9,
+          },
+        },
+        "text": undefined,
+      },
+    },
+  },
+]
+`;
diff --git a/test/parser/statement/__snapshots__/Stop.test.js.snap b/test/parser/statement/__snapshots__/Stop.test.js.snap
new file mode 100644
index 0000000..82c6e42
--- /dev/null
+++ b/test/parser/statement/__snapshots__/Stop.test.js.snap
@@ -0,0 +1,282 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`stop statement can be used as an object property 1`] = `
+Array [
+  Function {
+    "func": Function {
+      "body": Block {
+        "startingLocation": Object {
+          "end": Object {
+            "column": 25,
+            "line": 3,
+          },
+          "file": "",
+          "start": Object {
+            "column": 16,
+            "line": 3,
+          },
+        },
+        "statements": Array [
+          Assignment {
+            "name": Object {
+              "isReserved": false,
+              "kind": "Identifier",
+              "literal": undefined,
+              "location": Object {
+                "end": Object {
+                  "column": 25,
+                  "line": 3,
+                },
+                "file": "",
+                "start": Object {
+                  "column": 16,
+                  "line": 3,
+                },
+              },
+              "text": "theObject",
+            },
+            "tokens": Object {
+              "equals": Object {
+                "isReserved": false,
+                "kind": "Equal",
+                "literal": undefined,
+                "location": Object {
+                  "end": Object {
+                    "column": 27,
+                    "line": 3,
+                  },
+                  "file": "",
+                  "start": Object {
+                    "column": 26,
+                    "line": 3,
+                  },
+                },
+                "text": "=",
+              },
+            },
+            "value": AALiteral {
+              "close": Object {
+                "isReserved": false,
+                "kind": "RightBrace",
+                "literal": undefined,
+                "location": Object {
+                  "end": Object {
+                    "column": 17,
+                    "line": 5,
+                  },
+                  "file": "",
+                  "start": Object {
+                    "column": 16,
+                    "line": 5,
+                  },
+                },
+                "text": "}",
+              },
+              "elements": Array [
+                Object {
+                  "name": BrsString {
+                    "kind": 2,
+                    "value": "stop",
+                  },
+                  "value": Literal {
+                    "_location": Object {
+                      "end": Object {
+                        "column": 31,
+                        "line": 4,
+                      },
+                      "file": "",
+                      "start": Object {
+                        "column": 26,
+                        "line": 4,
+                      },
+                    },
+                    "value": BrsBoolean {
+                      "kind": 1,
+                      "value": false,
+                    },
+                  },
+                },
+              ],
+              "open": Object {
+                "isReserved": false,
+                "kind": "LeftBrace",
+                "literal": undefined,
+                "location": Object {
+                  "end": Object {
+                    "column": 29,
+                    "line": 3,
+                  },
+                  "file": "",
+                  "start": Object {
+                    "column": 28,
+                    "line": 3,
+                  },
+                },
+                "text": "{",
+              },
+            },
+          },
+          DottedSet {
+            "name": Object {
+              "isReserved": true,
+              "kind": "Identifier",
+              "literal": undefined,
+              "location": Object {
+                "end": Object {
+                  "column": 30,
+                  "line": 6,
+                },
+                "file": "",
+                "start": Object {
+                  "column": 26,
+                  "line": 6,
+                },
+              },
+              "text": "stop",
+            },
+            "obj": Variable {
+              "name": Object {
+                "isReserved": false,
+                "kind": "Identifier",
+                "literal": undefined,
+                "location": Object {
+                  "end": Object {
+                    "column": 25,
+                    "line": 6,
+                  },
+                  "file": "",
+                  "start": Object {
+                    "column": 16,
+                    "line": 6,
+                  },
+                },
+                "text": "theObject",
+              },
+            },
+            "value": Literal {
+              "_location": Object {
+                "end": Object {
+                  "column": 37,
+                  "line": 6,
+                },
+                "file": "",
+                "start": Object {
+                  "column": 33,
+                  "line": 6,
+                },
+              },
+              "value": BrsBoolean {
+                "kind": 1,
+                "value": true,
+              },
+            },
+          },
+        ],
+      },
+      "end": Object {
+        "isReserved": false,
+        "kind": "EndSub",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": 19,
+            "line": 7,
+          },
+          "file": "",
+          "start": Object {
+            "column": 12,
+            "line": 7,
+          },
+        },
+        "text": "end sub",
+      },
+      "keyword": Object {
+        "isReserved": true,
+        "kind": "Sub",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": 15,
+            "line": 2,
+          },
+          "file": "",
+          "start": Object {
+            "column": 12,
+            "line": 2,
+          },
+        },
+        "text": "sub",
+      },
+      "parameters": Array [],
+      "returns": 10,
+    },
+    "name": Object {
+      "isReserved": false,
+      "kind": "Identifier",
+      "literal": undefined,
+      "location": Object {
+        "end": Object {
+          "column": 20,
+          "line": 2,
+        },
+        "file": "",
+        "start": Object {
+          "column": 16,
+          "line": 2,
+        },
+      },
+      "text": "Main",
+    },
+  },
+]
+`;
+
+exports[`stop statement cannot be used as a local variable 1`] = `
+Array [
+  Stop {
+    "tokens": Object {
+      "stop": Object {
+        "isReserved": true,
+        "kind": "Stop",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": -9,
+            "line": -9,
+          },
+          "start": Object {
+            "column": -9,
+            "line": -9,
+          },
+        },
+        "text": "stop",
+      },
+    },
+  },
+]
+`;
+
+exports[`stop statement is valid as a statement 1`] = `
+Array [
+  Stop {
+    "tokens": Object {
+      "stop": Object {
+        "isReserved": true,
+        "kind": "Stop",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": -9,
+            "line": -9,
+          },
+          "start": Object {
+            "column": -9,
+            "line": -9,
+          },
+        },
+        "text": "stop",
+      },
+    },
+  },
+]
+`;