Repository: sjbarag/brs
Pull request #531 (2020-09-04 21:20:14)

implement dim statement (#283)
This implements the dim statement for as requested for #283. It doesn't match Roku's spec exactly as Roku's dim will leave the bottom-level arrays unexpanded but resizeable. When brs supports resizeable arrays (#530) we can come back and update this code to match the spec. 


Patch:
diff --git a/src/brsTypes/components/BrsObjects.ts b/src/brsTypes/components/BrsObjects.ts
index 33001ef..495f5d2 100644
--- a/src/brsTypes/components/BrsObjects.ts
+++ b/src/brsTypes/components/BrsObjects.ts
@@ -32,7 +32,7 @@ export const BrsObjects = new Map<string, Function>([
         "roregex",
         (_: Interpreter, expression: BrsString, flags: BrsString) => new RoRegex(expression, flags),
     ],
-    ["rostring", (_: Interpreter, literal: BrsString) => new RoString(literal)],
+    ["rostring", (_: Interpreter) => new RoString()],
     ["roboolean", (_: Interpreter, literal: BrsBoolean) => new roBoolean(literal)],
     ["rodouble", (_: Interpreter, literal: Double) => new roDouble(literal)],
     ["rofloat", (_: Interpreter, literal: Float) => new roFloat(literal)],
diff --git a/src/brsTypes/components/RoString.ts b/src/brsTypes/components/RoString.ts
index 4cedcd9..e537c04 100644
--- a/src/brsTypes/components/RoString.ts
+++ b/src/brsTypes/components/RoString.ts
@@ -10,16 +10,19 @@ import { Float } from "../Float";
 
 export class RoString extends BrsComponent implements BrsValue, Comparable, Unboxable {
     readonly kind = ValueKind.Object;
-    private intrinsic: BrsString;
+    private intrinsic: BrsString = new BrsString("");
 
     public getValue(): string {
         return this.intrinsic.value;
     }
 
-    constructor(initialValue: BrsString) {
+    constructor(initialValue?: BrsString) {
         super("roString");
 
-        this.intrinsic = initialValue;
+        if (initialValue) {
+            this.intrinsic = initialValue;
+        }
+
         this.registerMethods({
             ifString: [this.setString, this.getString],
             ifStringOps: [
@@ -34,6 +37,7 @@ export class RoString extends BrsComponent implements BrsValue, Comparable, Unbo
                 this.toInt,
                 this.toFloat,
                 this.tokenize,
+                this.setString,
                 this.split,
                 this.getEntityEncode,
                 this.escape,
@@ -91,21 +95,36 @@ export class RoString extends BrsComponent implements BrsValue, Comparable, Unbo
         return this.intrinsic.toString();
     }
 
-    // ---------- ifStringOps ----------
-    /** Sets the string to the first len characters of s. */
-    private setString = new Callable("SetString", {
-        signature: {
-            args: [
-                new StdlibArgument("s", ValueKind.String),
-                new StdlibArgument("len", ValueKind.Int32),
-            ],
-            returns: ValueKind.Void,
-        },
-        impl: (_interpreter, s: BrsString, len: Int32) => {
-            this.intrinsic = new BrsString(s.value.substr(0, len.getValue()));
-            return BrsInvalid.Instance;
+    /**
+     * Sets the string to the first len characters of s.
+     * Note: this method is implemented in the ifString and ifStringOps interfaces
+     */
+    private setString = new Callable(
+        "SetString",
+        {
+            signature: {
+                args: [new StdlibArgument("s", ValueKind.String)],
+                returns: ValueKind.Void,
+            },
+            impl: (_interpreter, s: BrsString) => {
+                this.intrinsic = new BrsString(s.value);
+                return BrsInvalid.Instance;
+            },
         },
-    });
+        {
+            signature: {
+                args: [
+                    new StdlibArgument("s", ValueKind.String),
+                    new StdlibArgument("len", ValueKind.Int32),
+                ],
+                returns: ValueKind.Void,
+            },
+            impl: (_interpreter, s: BrsString, len: Int32) => {
+                this.intrinsic = new BrsString(s.value.substr(0, len.getValue()));
+                return BrsInvalid.Instance;
+            },
+        }
+    );
 
     private getString = new Callable("GetString", {
         signature: {
@@ -115,6 +134,7 @@ export class RoString extends BrsComponent implements BrsValue, Comparable, Unbo
         impl: (_interpreter) => this.intrinsic,
     });
 
+    // ---------- ifStringOps ----------
     /** Appends the first len characters of s to the end of the string. */
     private appendString = new Callable("AppendString", {
         signature: {
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index a38ff72..d09e7a1 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -421,6 +421,56 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
         return BrsInvalid.Instance;
     }
 
+    visitDim(statement: Stmt.Dim): BrsType {
+        if (statement.name.isReserved) {
+            this.addError(
+                new BrsError(
+                    `Cannot assign a value to reserved name '${statement.name.text}'`,
+                    statement.name.location
+                )
+            );
+            return BrsInvalid.Instance;
+        }
+
+        // NOTE: Roku's dim implementation creates a resizeable, empty array for the
+        //   bottom children. Resizeable arrays aren't implemented yet (issue #530),
+        //   so when that's added this code should be updated so the bottom-level arrays
+        //   are resizeable, but empty
+        let dimensionValues: number[] = [];
+        statement.dimensions.forEach((expr) => {
+            let val = this.evaluate(expr);
+            if (val.kind !== ValueKind.Int32) {
+                this.addError(
+                    new BrsError(`Dim expression must evaluate to an integer`, expr.location)
+                );
+                return BrsInvalid.Instance;
+            }
+            // dim takes max-index, so +1 to get the actual array size
+            dimensionValues.push(val.getValue() + 1);
+            return;
+        });
+
+        let createArrayTree = (dimIndex: number = 0): RoArray => {
+            let children: RoArray[] = [];
+            let size = dimensionValues[dimIndex];
+            for (let i = 0; i < size; i++) {
+                if (dimIndex < dimensionValues.length) {
+                    let subchildren = createArrayTree(dimIndex + 1);
+                    if (subchildren !== undefined) children.push(subchildren);
+                }
+            }
+            let child = new RoArray(children);
+
+            return child;
+        };
+
+        let array = createArrayTree();
+
+        this.environment.define(Scope.Function, statement.name.text, array);
+
+        return BrsInvalid.Instance;
+    }
+
     visitBinary(expression: Expr.Binary) {
         let lexeme = expression.token.kind;
         let left = this.evaluate(expression.left);
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index 842c3a7..99099d0 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -612,6 +612,10 @@ export class Parser {
                 return returnStatement();
             }
 
+            if (check(Lexeme.Dim)) {
+                return dimStatement();
+            }
+
             if (check(Lexeme.Goto)) {
                 return gotoStatement();
             }
@@ -797,6 +801,7 @@ export class Parser {
             let thenToken: Token | undefined;
             let elseIfTokens: Token[] = [];
             let endIfToken: Token | undefined;
+            let elseToken: Token | undefined;
 
             /**
              * A simple wrapper around `check`, to make tests for a `then` identifier.
@@ -873,6 +878,7 @@ export class Parser {
                 }
 
                 if (blockEnd.kind === Lexeme.Else) {
+                    elseToken = blockEnd;
                     let maybeElseBranch = block(Lexeme.EndIf);
                     if (!maybeElseBranch) {
                         throw addError(peek(), "Expected 'end if' to terminate 'else' block");
@@ -952,6 +958,7 @@ export class Parser {
                     closingToken.kind !== Lexeme.Newline &&
                     (closingToken.kind === Lexeme.Else || match(Lexeme.Else))
                 ) {
+                    elseToken = closingToken;
                     let maybeElseBranch = block(Lexeme.Newline, Lexeme.Eof);
                     if (!maybeElseBranch) {
                         throw addError(peek(), `Expected a statement to follow 'else'`);
@@ -966,6 +973,7 @@ export class Parser {
                     then: thenToken,
                     elseIfs: elseIfTokens,
                     endIf: endIfToken,
+                    else: elseToken,
                 },
                 condition,
                 thenBranch,
@@ -1149,6 +1157,32 @@ export class Parser {
             return new Stmt.Label(tokens);
         }
 
+        /**
+         * Parses a `dim` statement
+         * @returns an AST representation of an `goto` statement.
+         */
+        function dimStatement() {
+            let dimToken = advance();
+
+            let name = consume("Expected variable name after 'dim'", Lexeme.Identifier);
+
+            match(Lexeme.LeftSquare);
+
+            let dimensions: Expression[] = [expression()];
+            while (!match(Lexeme.RightSquare)) {
+                consume("Expected ',' after expression in 'dim' statement", Lexeme.Comma);
+                dimensions.push(expression());
+            }
+            let rightSquare = previous();
+
+            let tokens = {
+                dim: dimToken,
+                closingBrace: rightSquare,
+            };
+
+            return new Stmt.Dim(tokens, name as Identifier, dimensions);
+        }
+
         /**
          * Parses a `goto` statement
          * @returns an AST representation of an `goto` statement.
diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts
index e4b5a8e..2800824 100644
--- a/src/parser/Statement.ts
+++ b/src/parser/Statement.ts
@@ -9,6 +9,7 @@ export * from "./BlockEndReason";
 
 export interface Visitor<T> {
     visitAssignment(statement: Assignment): BrsType;
+    visitDim(statement: Dim): BrsType;
     visitExpression(statement: Expression): BrsType;
     visitExitFor(statement: ExitFor): never;
     visitExitWhile(statement: ExitWhile): never;
@@ -64,6 +65,31 @@ export class Assignment extends AstNode implements Statement {
     }
 }
 
+export class Dim extends AstNode implements Statement {
+    constructor(
+        readonly tokens: {
+            dim: Token;
+            closingBrace: Token;
+        },
+        readonly name: Identifier,
+        readonly dimensions: Expr.Expression[]
+    ) {
+        super("Dim");
+    }
+
+    accept<R>(visitor: Visitor<R>): BrsType {
+        return visitor.visitDim(this);
+    }
+
+    get location() {
+        return {
+            file: this.tokens.dim.location.file,
+            start: this.tokens.dim.location.start,
+            end: this.tokens.closingBrace.location.end,
+        };
+    }
+}
+
 export class Block extends AstNode implements Statement {
     constructor(readonly statements: ReadonlyArray<Statement>, readonly location: Location) {
         super("Block");


Test patch:
diff --git a/test/brsTypes/components/RoString.test.js b/test/brsTypes/components/RoString.test.js
index a315687..c3314db 100644
--- a/test/brsTypes/components/RoString.test.js
+++ b/test/brsTypes/components/RoString.test.js
@@ -3,6 +3,13 @@ const { Int32, Float, BrsString, RoString, RoArray, BrsBoolean, Callable } = brs
 const { Interpreter } = require("../../../lib/interpreter");
 
 describe("RoString", () => {
+    describe("constructor", () => {
+        it("starts with empty string when no arg is passed to constructor", () => {
+            let a = new RoString();
+            expect(a.equalTo(new BrsString(""))).toBe(BrsBoolean.True);
+        });
+    });
+
     describe("equality", () => {
         it("compares to intrinsic strings", () => {
             let a = new RoString(new BrsString("foo"));
@@ -43,19 +50,15 @@ describe("RoString", () => {
                 expect(setString).toBeInstanceOf(Callable);
             });
 
-            it("overwrites its stored string", () => {
-                setString.call(interpreter, new BrsString("after"), new Int32(2));
-                expect(s.intrinsic).toEqual(new BrsString("af"));
+            it("sets a string into the object", () => {
+                setString.call(interpreter, new BrsString("hello"));
+                expect(s.intrinsic).toEqual(new BrsString("hello"));
             });
 
-            it("overwrites an empty string for zero `len`", () => {
-                setString.call(interpreter, new BrsString("after"), new Int32(0));
-                expect(s.intrinsic).toEqual(new BrsString(""));
-            });
-
-            it("overwrites an empty string for negative `len`", () => {
-                setString.call(interpreter, new BrsString("after"), new Int32(-1));
-                expect(s.intrinsic).toEqual(new BrsString(""));
+            it("overwrites string value previously set", () => {
+                setString.call(interpreter, new BrsString("hello"));
+                setString.call(interpreter, new BrsString("world"));
+                expect(s.intrinsic).toEqual(new BrsString("world"));
             });
         });
 
@@ -412,6 +415,31 @@ describe("RoString", () => {
             it.todo("splits by multi-character delimiter");
         });
 
+        describe("seString", () => {
+            let s, setString;
+
+            beforeEach(() => {
+                s = new RoString(new BrsString("before"));
+                setString = s.getMethod("setString");
+                expect(setString).toBeInstanceOf(Callable);
+            });
+
+            it("overwrites its stored string", () => {
+                setString.call(interpreter, new BrsString("after"), new Int32(2));
+                expect(s.intrinsic).toEqual(new BrsString("af"));
+            });
+
+            it("overwrites an empty string for zero `len`", () => {
+                setString.call(interpreter, new BrsString("after"), new Int32(0));
+                expect(s.intrinsic).toEqual(new BrsString(""));
+            });
+
+            it("overwrites an empty string for negative `len`", () => {
+                setString.call(interpreter, new BrsString("after"), new Int32(-1));
+                expect(s.intrinsic).toEqual(new BrsString(""));
+            });
+        });
+
         describe("split", () => {
             let split;
 
diff --git a/test/e2e/BrsComponents.test.js b/test/e2e/BrsComponents.test.js
index 615d389..359eaab 100644
--- a/test/e2e/BrsComponents.test.js
+++ b/test/e2e/BrsComponents.test.js
@@ -296,8 +296,10 @@ describe("end to end brightscript functions", () => {
 
         expect(allArgs(outputStreams.stderr.write)).toEqual([]);
         expect(allArgs(outputStreams.stdout.write).filter((arg) => arg !== "\n")).toEqual([
+            "hello",
             "bar",
             "bar",
+            "foo",
             "true", // comparison
             "5", // length
             "b", // split("/")[1]
diff --git a/test/e2e/Syntax.test.js b/test/e2e/Syntax.test.js
index 6659a06..ade1709 100644
--- a/test/e2e/Syntax.test.js
+++ b/test/e2e/Syntax.test.js
@@ -120,6 +120,19 @@ describe("end to end syntax", () => {
         ]);
     });
 
+    test("dim.brs", async () => {
+        await execute([resourceFile("dim.brs")], outputStreams);
+
+        expect(allArgs(outputStreams.stdout.write).filter((arg) => arg !== "\n")).toEqual([
+            "4",
+            "5",
+            "5",
+            "hello",
+            "invalid",
+            "invalid",
+        ]);
+    });
+
     test("while-loops.brs", async () => {
         await execute([resourceFile("while-loops.brs")], outputStreams);
 
diff --git a/test/e2e/resources/components/roString.brs b/test/e2e/resources/components/roString.brs
index 2b06063..0f4c98b 100644
--- a/test/e2e/resources/components/roString.brs
+++ b/test/e2e/resources/components/roString.brs
@@ -1,12 +1,18 @@
 sub main()
     ' direct creation
-    r = createObject("RoString", "foo")
+    r = createObject("RoString")
+
+    r.appendString("hello", 5) ' appends hello to the default empty string
+    print r.getString() ' => "hello"
 
     s = "bar"
     print s.getString() ' => "bar"
     print s.toStr() ' => "bar" (again)
 
-    r.setString("boo!", 1)
+    r.setString("foo") ' setString in the ifString interface
+    print r.getString() ' => "foo"
+
+    r.setString("boo!", 1) ' setString in the ifStringOps interface, replaces old value
     r.appendString("ar", 10)
 
     ' comparisons
diff --git a/test/e2e/resources/dim.brs b/test/e2e/resources/dim.brs
new file mode 100644
index 0000000..1a0784e
--- /dev/null
+++ b/test/e2e/resources/dim.brs
@@ -0,0 +1,17 @@
+' initialize foo to some base value
+dim array[3,4]
+
+' NOTE: Roku's dim implementation creates a resizeable, empty array for the
+'   bottom children. Resizeable arrays aren't implemented yet (issue #530), 
+'   so when that's added this code should be updated so the final array size
+'   matches the index of what it expanded to after assignment
+print array.count()
+print array[0].count()
+print array[3].count()
+
+array[3][4] = "hello"
+print array[3][4]
+
+print array[4]
+print array[3][5]
+
diff --git a/test/interpreter/Dim.test.js b/test/interpreter/Dim.test.js
new file mode 100644
index 0000000..6cbbcd4
--- /dev/null
+++ b/test/interpreter/Dim.test.js
@@ -0,0 +1,115 @@
+const Expr = require("../../lib/parser/Expression");
+const Stmt = require("../../lib/parser/Statement");
+const { Interpreter } = require("../../lib/interpreter");
+const brs = require("brs");
+const { Lexeme } = brs.lexer;
+const { Int32, BrsString } = brs.types;
+
+const { token, identifier, fakeLocation } = require("../parser/ParserTests");
+
+let interpreter;
+
+const LEFT_SQUARE = token(Lexeme.LeftSquare, "[");
+const RIGHT_SQUARE = token(Lexeme.RightSquare, "]");
+const LEFT_BRACE = token(Lexeme.LeftBrace, "{");
+const RIGHT_BRACE = token(Lexeme.RightBrace, "}");
+
+describe("creating arrays using dim", () => {
+    beforeEach(() => {
+        interpreter = new Interpreter();
+    });
+
+    test("one-dimensional creation", () => {
+        let maxIndex = 5;
+        let statements = [
+            new Stmt.Dim(
+                {
+                    dim: token(Lexeme.Dim, "dim"),
+                    closingBrace: token(Lexeme.RightSquare, "closingSquare"),
+                },
+                identifier("array"),
+                [new Expr.Literal(new Int32(maxIndex))]
+            ),
+            new Stmt.IndexedSet(
+                new Expr.Variable(identifier("array")),
+                new Expr.Literal(new Int32(4), fakeLocation),
+                new Expr.Literal(new BrsString("new index4"), fakeLocation),
+                RIGHT_SQUARE
+            ),
+            new Stmt.Assignment(
+                { equals: token(Lexeme.Equals, "=") },
+                identifier("result"),
+                new Expr.IndexedGet(
+                    new Expr.Variable(identifier("array")),
+                    new Expr.Literal(new Int32(4), fakeLocation),
+                    RIGHT_SQUARE
+                )
+            ),
+        ];
+
+        interpreter.exec(statements);
+        expect(interpreter.environment.get(identifier("result"))).toEqual(
+            new BrsString("new index4")
+        );
+
+        // NOTE: Roku's dim implementation creates a resizeable, empty array for the
+        //   bottom children. Resizeable arrays aren't implemented yet (issue #530),
+        //   so when that's added this code should be updated so the final array size
+        //   matches the index of what it expanded to after assignment
+        let arr = interpreter.environment.get(identifier("array"));
+        let count = arr.getMethod("count");
+        expect(count.call(interpreter)).toEqual(new Int32(maxIndex + 1));
+    });
+
+    test("multi-dimensional creation", () => {
+        let maxIndex1 = 6;
+        let maxIndex2 = 3;
+        let statements = [
+            new Stmt.Dim(
+                {
+                    dim: token(Lexeme.Dim, "dim"),
+                    closingBrace: token(Lexeme.RightSquare, "closingSquare"),
+                },
+                identifier("baseArray"),
+                [new Expr.Literal(new Int32(maxIndex1)), new Expr.Literal(new Int32(maxIndex2))]
+            ),
+            new Stmt.IndexedSet(
+                new Expr.IndexedGet(
+                    new Expr.Variable(identifier("baseArray")),
+                    new Expr.Literal(new Int32(2), fakeLocation),
+                    RIGHT_SQUARE
+                ),
+                new Expr.Literal(new Int32(1), fakeLocation),
+                new Expr.Literal(new BrsString("new (2,1)"), fakeLocation),
+                RIGHT_SQUARE
+            ),
+            new Stmt.Assignment(
+                { equals: token(Lexeme.Equals, "=") },
+                identifier("result"),
+                new Expr.IndexedGet(
+                    new Expr.IndexedGet(
+                        new Expr.Variable(identifier("baseArray")),
+                        new Expr.Literal(new Int32(2), fakeLocation),
+                        RIGHT_SQUARE
+                    ),
+                    new Expr.Literal(new Int32(1), fakeLocation),
+                    RIGHT_SQUARE
+                )
+            ),
+        ];
+
+        interpreter.exec(statements);
+        expect(interpreter.environment.has(identifier("baseArray"))).toBe(true);
+        expect(interpreter.environment.get(identifier("result"))).toEqual(
+            new BrsString("new (2,1)")
+        );
+
+        // NOTE: Roku's dim implementation creates a resizeable, empty array for the
+        //   bottom children. Resizeable arrays aren't implemented yet (issue #530),
+        //   so when that's added this code should be updated so the final array size
+        //   matches the index of what it expanded to after assignment
+        let arr = interpreter.environment.get(identifier("baseArray"));
+        let count = arr.getMethod("count");
+        expect(count.call(interpreter)).toEqual(new Int32(maxIndex1 + 1));
+    });
+});
diff --git a/test/parser/controlFlow/__snapshots__/If.test.js.snap b/test/parser/controlFlow/__snapshots__/If.test.js.snap
index 30f2eee..af1d61c 100644
--- a/test/parser/controlFlow/__snapshots__/If.test.js.snap
+++ b/test/parser/controlFlow/__snapshots__/If.test.js.snap
@@ -397,6 +397,23 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": Object {
+        "isReserved": true,
+        "kind": "Else",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": 20,
+            "line": 7,
+          },
+          "file": "",
+          "start": Object {
+            "column": 16,
+            "line": 7,
+          },
+        },
+        "text": "else",
+      },
       "elseIfs": Array [
         Object {
           "isReserved": false,
@@ -649,6 +666,7 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": undefined,
       "elseIfs": Array [],
       "endIf": Object {
         "isReserved": false,
@@ -971,6 +989,23 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": Object {
+        "isReserved": true,
+        "kind": "Else",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": 20,
+            "line": 4,
+          },
+          "file": "",
+          "start": Object {
+            "column": 16,
+            "line": 4,
+          },
+        },
+        "text": "else",
+      },
       "elseIfs": Array [],
       "endIf": Object {
         "isReserved": false,
@@ -1426,6 +1461,23 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": Object {
+        "isReserved": true,
+        "kind": "Else",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": 20,
+            "line": 7,
+          },
+          "file": "",
+          "start": Object {
+            "column": 16,
+            "line": 7,
+          },
+        },
+        "text": "else",
+      },
       "elseIfs": Array [
         Object {
           "isReserved": false,
@@ -1736,6 +1788,23 @@ Array [
               "type": "Block",
             },
             "tokens": Object {
+              "else": Object {
+                "isReserved": true,
+                "kind": "Else",
+                "literal": undefined,
+                "location": Object {
+                  "end": Object {
+                    "column": 24,
+                    "line": 7,
+                  },
+                  "file": "",
+                  "start": Object {
+                    "column": 20,
+                    "line": 7,
+                  },
+                },
+                "text": "else",
+              },
               "elseIfs": Array [
                 Object {
                   "isReserved": false,
@@ -1990,6 +2059,7 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": undefined,
       "elseIfs": Array [],
       "endIf": undefined,
       "if": Object {
@@ -2134,6 +2204,7 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": undefined,
       "elseIfs": Array [],
       "endIf": Object {
         "isReserved": false,
@@ -2471,6 +2542,23 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": Object {
+        "isReserved": true,
+        "kind": "Else",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": 40,
+            "line": 2,
+          },
+          "file": "",
+          "start": Object {
+            "column": 36,
+            "line": 2,
+          },
+        },
+        "text": "else",
+      },
       "elseIfs": Array [],
       "endIf": Object {
         "isReserved": false,
@@ -2724,6 +2812,23 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": Object {
+        "isReserved": true,
+        "kind": "Else",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": 50,
+            "line": 2,
+          },
+          "file": "",
+          "start": Object {
+            "column": 46,
+            "line": 2,
+          },
+        },
+        "text": "else",
+      },
       "elseIfs": Array [],
       "endIf": undefined,
       "if": Object {
@@ -3091,6 +3196,22 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": Object {
+        "isReserved": true,
+        "kind": "Else",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": -9,
+            "line": -9,
+          },
+          "start": Object {
+            "column": -9,
+            "line": -9,
+          },
+        },
+        "text": "else",
+      },
       "elseIfs": Array [
         Object {
           "isReserved": false,
@@ -3278,6 +3399,7 @@ Array [
             "type": "Block",
           },
           "tokens": Object {
+            "else": undefined,
             "elseIfs": Array [],
             "endIf": undefined,
             "if": Object {
@@ -3321,6 +3443,23 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": Object {
+        "isReserved": true,
+        "kind": "Else",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": 16,
+            "line": 4,
+          },
+          "file": "",
+          "start": Object {
+            "column": 12,
+            "line": 4,
+          },
+        },
+        "text": "else",
+      },
       "elseIfs": Array [],
       "endIf": Object {
         "isReserved": false,
@@ -4292,6 +4431,7 @@ Array [
             "type": "Block",
           },
           "tokens": Object {
+            "else": undefined,
             "elseIfs": Array [],
             "endIf": undefined,
             "if": Object {
@@ -4335,6 +4475,23 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": Object {
+        "isReserved": true,
+        "kind": "Else",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": 16,
+            "line": 7,
+          },
+          "file": "",
+          "start": Object {
+            "column": 12,
+            "line": 7,
+          },
+        },
+        "text": "else",
+      },
       "elseIfs": Array [
         Object {
           "isReserved": false,
@@ -4524,6 +4681,7 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": undefined,
       "elseIfs": Array [],
       "endIf": undefined,
       "if": Object {
@@ -4762,6 +4920,22 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": Object {
+        "isReserved": true,
+        "kind": "Else",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": -9,
+            "line": -9,
+          },
+          "start": Object {
+            "column": -9,
+            "line": -9,
+          },
+        },
+        "text": "else",
+      },
       "elseIfs": Array [],
       "endIf": undefined,
       "if": Object {
@@ -5127,6 +5301,22 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": Object {
+        "isReserved": true,
+        "kind": "Else",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": -9,
+            "line": -9,
+          },
+          "start": Object {
+            "column": -9,
+            "line": -9,
+          },
+        },
+        "text": "else",
+      },
       "elseIfs": Array [
         Object {
           "isReserved": false,
@@ -5266,6 +5456,7 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": undefined,
       "elseIfs": Array [],
       "endIf": Object {
         "isReserved": false,
@@ -5527,6 +5718,23 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": Object {
+        "isReserved": true,
+        "kind": "Else",
+        "literal": undefined,
+        "location": Object {
+          "end": Object {
+            "column": 74,
+            "line": 2,
+          },
+          "file": "",
+          "start": Object {
+            "column": 70,
+            "line": 2,
+          },
+        },
+        "text": "else",
+      },
       "elseIfs": Array [
         Object {
           "isReserved": false,
@@ -5705,6 +5913,7 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": undefined,
       "elseIfs": Array [],
       "endIf": Object {
         "isReserved": false,
@@ -5923,6 +6132,7 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": undefined,
       "elseIfs": Array [],
       "endIf": Object {
         "isReserved": false,
@@ -6291,6 +6501,23 @@ Array [
               "type": "Block",
             },
             "tokens": Object {
+              "else": Object {
+                "isReserved": true,
+                "kind": "Else",
+                "literal": undefined,
+                "location": Object {
+                  "end": Object {
+                    "column": 20,
+                    "line": 7,
+                  },
+                  "file": "",
+                  "start": Object {
+                    "column": 16,
+                    "line": 7,
+                  },
+                },
+                "text": "else",
+              },
               "elseIfs": Array [
                 Object {
                   "isReserved": false,
@@ -6529,6 +6756,7 @@ Array [
       "type": "Block",
     },
     "tokens": Object {
+      "else": undefined,
       "elseIfs": Array [],
       "endIf": Object {
         "isReserved": false,
diff --git a/test/parser/statement/Dim.test.js b/test/parser/statement/Dim.test.js
new file mode 100644
index 0000000..696e500
--- /dev/null
+++ b/test/parser/statement/Dim.test.js
@@ -0,0 +1,30 @@
+const brs = require("brs");
+const { token, identifier, EOF } = require("../ParserTests");
+const { Lexeme } = brs.lexer;
+const { Expr, Stmt } = brs.parser;
+
+describe("parser dim statements", () => {
+    it("parses one-dimension", () => {
+        let { statements, errors } = brs.parser.Parser.parse([
+            token(Lexeme.Dim, "dim"),
+            identifier("foo"),
+            token(Lexeme.LeftSquare, "["),
+            identifier("bar"), // any valid expression
+            token(Lexeme.RightSquare, "]"),
+            EOF,
+        ]);
+        expect(errors.length).toEqual(0);
+    });
+
+    it("errors when no dimensions provided", () => {
+        let { statements, errors } = brs.parser.Parser.parse([
+            token(Lexeme.Dim, "dim"),
+            identifier("foo"),
+            token(Lexeme.LeftSquare, "["),
+            // missing dimension, throw error
+            token(Lexeme.RightSquare, "]"),
+            EOF,
+        ]);
+        expect(errors.length).toEqual(1);
+    });
+});
diff --git a/test/parser/statement/__snapshots__/Block.test.js.snap b/test/parser/statement/__snapshots__/Block.test.js.snap
index 7e807d1..067c3ad 100644
--- a/test/parser/statement/__snapshots__/Block.test.js.snap
+++ b/test/parser/statement/__snapshots__/Block.test.js.snap
@@ -677,6 +677,23 @@ Array [
               "type": "Block",
             },
             "tokens": Object {
+              "else": Object {
+                "isReserved": true,
+                "kind": "Else",
+                "literal": undefined,
+                "location": Object {
+                  "end": Object {
+                    "column": 20,
+                    "line": 7,
+                  },
+                  "file": "",
+                  "start": Object {
+                    "column": 16,
+                    "line": 7,
+                  },
+                },
+                "text": "else",
+              },
               "elseIfs": Array [
                 Object {
                   "isReserved": false,