Repository: sjbarag/brs
Pull request #405 (2020-05-01 21:57:38)

feat(stdlib): Implement Group Component


Patch:
diff --git a/.editorconfig b/.editorconfig
index 97b0312..d33534e 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -5,3 +5,6 @@ charset = utf-8
 indent_style = space
 indent_size = 4
 trim_trailing_whitespace = true
+
+[*.test.js]
+trim_trailing_whitespace = false
diff --git a/README.md b/README.md
index f501899..a8417db 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,12 @@ $ ls types/
 index.d.ts (and friends)
 ```
 
+Alternatively, you can run the build step in "watch" mode. This will run `yarn build` for you automatically, every time it detects source file changes:
+```shell
+$ yarn watch
+```
+This is often useful for testing that local changes work in your BrightScript project, without having to run `yarn build` over and over.
+
 #### Testing
 Tests are written in plain-old JavaScript with [Facebook's Jest](http://facebook.github.io/jest/), and can be run with the `test` target:
 
diff --git a/package.json b/package.json
index 9dffb8c..8b7f67d 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
   },
   "scripts": {
     "build": "tsc",
+    "watch": "tsc -w",
     "clean": "rimraf ./lib ./types",
     "test": "jest",
     "lint": "tslint --project .",
diff --git a/src/brsTypes/components/Group.ts b/src/brsTypes/components/Group.ts
new file mode 100644
index 0000000..1e7c93e
--- /dev/null
+++ b/src/brsTypes/components/Group.ts
@@ -0,0 +1,28 @@
+import { RoSGNode, FieldModel } from "./RoSGNode";
+import { AAMember } from "./RoAssociativeArray";
+
+export class Group extends RoSGNode {
+    readonly defaultFields: FieldModel[] = [
+        { name: "visible", type: "boolean", value: "true" },
+        { name: "opacity", type: "float", value: "1.0" },
+        { name: "translation", type: "array", value: "[0.0,0.0]" },
+        { name: "rotation", type: "float", value: "0.0" },
+        { name: "scale", type: "array", value: "[1.0,1.0]" },
+        { name: "scaleRotateCenter", type: "array", value: "[0.0,0.0]" },
+        { name: "childRenderOrder", type: "string", value: "renderLast" },
+        { name: "inheritParentTransform", type: "boolean", value: "true" },
+        { name: "inheritParentOpacity", type: "boolean", value: "true" },
+        { name: "clippingRect", type: "array", value: "[0.0,0.0,0.0,0.0]" },
+        { name: "renderPass", type: "integer", value: "0" },
+        { name: "muteAudioGuide", type: "boolean", value: "false" },
+        { name: "enableRenderTracking", type: "boolean", value: "false" },
+        { name: "renderTracking", type: "string", value: "disabled" },
+    ];
+
+    constructor(initializedFields: AAMember[] = [], readonly name: string = "Group") {
+        super([], name);
+
+        this.registerDefaultFields(this.defaultFields);
+        this.registerInitializedFields(initializedFields);
+    }
+}
diff --git a/src/brsTypes/components/RoSGNode.ts b/src/brsTypes/components/RoSGNode.ts
index 9081ed4..66ef2c2 100644
--- a/src/brsTypes/components/RoSGNode.ts
+++ b/src/brsTypes/components/RoSGNode.ts
@@ -8,20 +8,22 @@ import {
     getBrsValueFromFieldType,
 } from "../BrsType";
 import { BrsComponent, BrsIterable } from "./BrsComponent";
-import { BrsType } from "..";
+import { BrsType, BrsBuiltInComponents } from "..";
 import { Callable, StdlibArgument } from "../Callable";
 import { Interpreter } from "../../interpreter";
 import { Int32 } from "../Int32";
 import { RoAssociativeArray } from "./RoAssociativeArray";
 import { RoArray } from "./RoArray";
 import { AAMember } from "./RoAssociativeArray";
-import { Float } from "../Float";
 import { ComponentDefinition } from "../../componentprocessor";
 
 interface BrsCallback {
     interpreter: Interpreter;
     callable: Callable;
 }
+
+export type FieldModel = { name: string; type: string; value?: string };
+
 class Field {
     private type: string;
     private value: BrsType;
@@ -69,27 +71,21 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
     private fields = new Map<string, Field>();
     private children: RoSGNode[] = [];
     private parent: RoSGNode | BrsInvalid = BrsInvalid.Instance;
-    readonly builtInFields = [
+    readonly defaultFields: FieldModel[] = [
         { name: "change", type: "roAssociativeArray" },
         { name: "focusable", type: "boolean" },
         { name: "focusedChild", type: "node" },
         { name: "id", type: "string" },
     ];
 
-    constructor(members: AAMember[], readonly type: string = "Node") {
-        super("roSGNode");
+    constructor(initializedFields: AAMember[], readonly type: string = "Node") {
+        super(type);
 
-        // All nodes start have some built-in fields when created
-        this.builtInFields.forEach(field => {
-            this.fields.set(
-                field.name.toLowerCase(),
-                new Field(getBrsValueFromFieldType(field.type), false)
-            );
-        });
+        // All nodes start have some built-in fields when created.
+        this.registerDefaultFields(this.defaultFields);
 
-        members.forEach(member =>
-            this.fields.set(member.name.value.toLowerCase(), new Field(member.value, false))
-        );
+        // After registering default fields, then register fields instantiated with initial values.
+        this.registerInitializedFields(initializedFields);
 
         this.registerMethods({
             ifAssociativeArray: [
@@ -1073,47 +1069,82 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable {
             return new BrsString(this.type);
         },
     });
+
+    /* Takes a list of models and creates fields with default values, and adds them to this.fields. */
+    protected registerDefaultFields(fields: FieldModel[]) {
+        fields.forEach(field => {
+            this.fields.set(
+                field.name.toLowerCase(),
+                new Field(getBrsValueFromFieldType(field.type, field.value), false)
+            );
+        });
+    }
+
+    /**
+     * Takes a list of preset fields and creates fields from them.
+     * TODO: filter out any non-allowed members. For example, if we instantiate a Node like this:
+     *      <Node thisisnotanodefield="fakevalue" />
+     * then Roku logs an error, because Node does not have a property called "thisisnotanodefield".
+     */
+    protected registerInitializedFields(fields: AAMember[]) {
+        fields.forEach(field =>
+            this.fields.set(field.name.value.toLowerCase(), new Field(field.value, false))
+        );
+    }
 }
 
-export function createNodeByType(interpreter: Interpreter, type: BrsString) {
+export function createNodeByType(interpreter: Interpreter, type: BrsString): RoSGNode | BrsInvalid {
     let typeDef = interpreter.environment.nodeDefMap.get(type.value);
-    if (type.value === "Node") {
-        return new RoSGNode([]);
+    let builtInTypeCtor = BrsBuiltInComponents.get(type.value);
+
+    if (builtInTypeCtor) {
+        return builtInTypeCtor();
     } else if (typeDef) {
         //use typeDef object to tack on all the bells & whistles of a custom node
-        let node = new RoSGNode([], type.value);
         let typeDefStack: ComponentDefinition[] = [];
         let currentEnv = typeDef.environment;
 
         // Adding all component extensions to the stack to call init methods
         // in the correct order.
         typeDefStack.push(typeDef);
-        while (typeDef && typeDef.extends) {
+        while (typeDef) {
             typeDef = interpreter.environment.nodeDefMap.get(typeDef.extends);
             if (typeDef) typeDefStack.push(typeDef);
         }
 
+        // Start from the "basemost" component of the tree.
+        typeDef = typeDefStack.pop();
+
+        // If this extends a built-in component other than Node, create it.
+        // Default to Node otherwise.
+        let node: RoSGNode = new RoSGNode([], type.value);
+        builtInTypeCtor = BrsBuiltInComponents.get(typeDef!.extends);
+        if (builtInTypeCtor) {
+            node = builtInTypeCtor(type.value);
+        }
+
         // Add children, fields and call each init method starting from the
         // "basemost" component of the tree.
-        while (typeDef && typeDefStack.length > 0) {
-            typeDef = typeDefStack.pop();
+        while (typeDef) {
             let init: BrsType;
 
-            if (typeDef) {
-                addChildren(interpreter, node, typeDef);
-                addFields(interpreter, node, typeDef);
-                interpreter.inSubEnv(subInterpreter => {
-                    init = subInterpreter.getInitMethod();
-                    return BrsInvalid.Instance;
-                }, typeDef.environment);
-            }
+            addChildren(interpreter, node, typeDef);
+            addFields(interpreter, node, typeDef);
+            interpreter.inSubEnv(subInterpreter => {
+                init = subInterpreter.getInitMethod();
+                return BrsInvalid.Instance;
+            }, typeDef.environment);
 
             interpreter.inSubEnv(subInterpreter => {
+                let mPointer = subInterpreter.environment.getM();
+                mPointer.set(new BrsString("top"), node);
                 if (init instanceof Callable) {
                     init.call(subInterpreter);
                 }
                 return BrsInvalid.Instance;
             }, currentEnv);
+
+            typeDef = typeDefStack.pop();
         }
 
         return node;
@@ -1127,7 +1158,7 @@ function addFields(interpreter: Interpreter, node: RoSGNode, typeDef: ComponentD
     for (let [key, value] of Object.entries(fields)) {
         if (value instanceof Object) {
             let addField = node.getMethod("addField");
-            let setField = node.getMethod("setFIeld");
+            let setField = node.getMethod("setField");
             const fieldName = new BrsString(key);
             if (addField) {
                 addField.call(
diff --git a/src/brsTypes/index.ts b/src/brsTypes/index.ts
index f45d626..4eff869 100644
--- a/src/brsTypes/index.ts
+++ b/src/brsTypes/index.ts
@@ -17,6 +17,8 @@ import { Callable } from "./Callable";
 import { BrsComponent } from "./components/BrsComponent";
 import { RoString } from "./components/RoString";
 import { BrsInterface } from "./BrsInterface";
+import { RoSGNode } from "./components/RoSGNode";
+import { Group } from "./components/Group";
 
 export * from "./BrsType";
 export * from "./Int32";
@@ -29,6 +31,7 @@ export * from "./components/RoDateTime";
 export * from "./components/RoAssociativeArray";
 export * from "./components/Timespan";
 export * from "./components/RoSGNode";
+export * from "./components/Group";
 export * from "./components/BrsObjects";
 export * from "./components/RoRegex";
 export * from "./components/RoString";
@@ -39,6 +42,14 @@ export * from "./components/RoInt";
 export * from "./components/RoInvalid";
 export * from "./Callable";
 
+// Allows for construction of new built-in objects.
+// TODO: update map with more entries as they're implemented (Group, Label, etc.)
+type NodeCtor = (nodeType?: string) => RoSGNode;
+export const BrsBuiltInComponents = new Map<string, NodeCtor>([
+    ["Node", (nodeType: string = "Node") => new RoSGNode([], nodeType)],
+    ["Group", (nodeType: string = "Group") => new Group([], nodeType)],
+]);
+
 /**
  * Determines whether or not the given value is a number.
  * @param value the BrightScript value in question.
diff --git a/src/extensions/RunInScope.ts b/src/extensions/RunInScope.ts
index 1ac2594..f736d91 100644
--- a/src/extensions/RunInScope.ts
+++ b/src/extensions/RunInScope.ts
@@ -37,18 +37,12 @@ function runFilesInScope(interpreter: Interpreter, filenames: BrsString[], args:
         return BrsInvalid.Instance;
     }
 
-    try {
-        let ast = brs.lexParseSync(pathsToFiles, interpreter.options);
-        return interpreter.inSubEnv(subInterpreter => {
-            // remove the original `main` function so we can execute the new file
-            subInterpreter.environment.remove("main", Scope.Module);
-            return subInterpreter.exec(ast, ...args)[0] || BrsInvalid.Instance;
-        });
-    } catch (err) {
-        // swallow errors and just return invalid; RBI returns invalid for "file doesn't exist" errors,
-        // syntax errors, etc.
-        return BrsInvalid.Instance;
-    }
+    let ast = brs.lexParseSync(pathsToFiles, interpreter.options);
+    return interpreter.inSubEnv(subInterpreter => {
+        // remove the original `main` function so we can execute the new file
+        subInterpreter.environment.remove("main", Scope.Module);
+        return subInterpreter.exec(ast, ...args)[0] || BrsInvalid.Instance;
+    });
 }
 
 export const RunInScope = new Callable(
diff --git a/src/index.ts b/src/index.ts
index 622811d..f7d579c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -83,11 +83,23 @@ export function lexParseSync(filenames: string[], options: Partial<ExecutionOpti
 
     return filenames
         .map(filename => {
-            let contents = fs.readFileSync(filename, "utf8");
-            let scanResults = Lexer.scan(contents, filename);
+            let lexer = new Lexer();
             let preprocessor = new PP.Preprocessor();
+            let parser = new Parser();
+            [lexer, preprocessor, parser].forEach(emitter =>
+                emitter.onError(BrsError.getLoggerUsing(executionOptions.stderr))
+            );
+
+            let contents = fs.readFileSync(filename, "utf8");
+            let scanResults = lexer.scan(contents, filename);
             let preprocessorResults = preprocessor.preprocess(scanResults.tokens, manifest);
-            return Parser.parse(preprocessorResults.processedTokens).statements;
+            let parseResults = parser.parse(preprocessorResults.processedTokens);
+
+            if (parseResults.errors.length > 0) {
+                throw "Error occurred parsing";
+            }
+
+            return parseResults.statements;
         })
         .reduce((allStatements, statements) => [...allStatements, ...statements], []);
 }
diff --git a/src/interpreter/Environment.ts b/src/interpreter/Environment.ts
index 7d58be3..7f7247a 100644
--- a/src/interpreter/Environment.ts
+++ b/src/interpreter/Environment.ts
@@ -113,6 +113,14 @@ export class Environment {
         return this.rootM;
     }
 
+    /**
+     * Sets the the special `m` variable from the root Environment.
+     * @param newMPointer the new value to be used for the `m` pointer
+     */
+    public setRootM(newMPointer: RoAssociativeArray): void {
+        this.rootM = newMPointer;
+    }
+
     /**
      * Removes a variable from this environment's function scope.
      * @param name the name of the variable to remove (in the form of an `Identifier`)
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index 9faad1c..d88a864 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -122,7 +122,9 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
                 );
                 let statements = await componentScopeResolver.resolve(component);
                 interpreter.inSubEnv(subInterpreter => {
-                    subInterpreter.environment.setM(interpreter.environment.getM());
+                    let componentMPointer = new RoAssociativeArray([]);
+                    subInterpreter.environment.setM(componentMPointer);
+                    subInterpreter.environment.setRootM(componentMPointer);
                     subInterpreter.exec(statements);
                     return BrsInvalid.Instance;
                 }, component.environment);
diff --git a/src/lexer/Lexer.ts b/src/lexer/Lexer.ts
index c302a35..88b0de2 100644
--- a/src/lexer/Lexer.ts
+++ b/src/lexer/Lexer.ts
@@ -692,7 +692,7 @@ export class Lexer {
                     advance();
                 } // read the next word
 
-                let twoWords = source.slice(start, current);
+                let twoWords = source.slice(start, current).toLowerCase();
                 switch (twoWords.replace(/ {2,}/g, " ")) {
                     case "#else if":
                         addToken(Lexeme.HashElseIf);
diff --git a/src/parser/ComponentScopeResolver.ts b/src/parser/ComponentScopeResolver.ts
index 78af4a9..0e6314b 100644
--- a/src/parser/ComponentScopeResolver.ts
+++ b/src/parser/ComponentScopeResolver.ts
@@ -1,5 +1,6 @@
 import { ComponentDefinition } from "../componentprocessor";
 import * as Stmt from "./Statement";
+import { BrsBuiltInComponents } from "../brsTypes";
 
 export class ComponentScopeResolver {
     private readonly excludedNames: string[] = ["init"];
@@ -63,6 +64,11 @@ export class ComponentScopeResolver {
         yield this.parserLexerFn(component.scripts.map(c => c.uri));
         let currentComponent: ComponentDefinition | undefined = component;
         while (currentComponent.extends) {
+            // If this is a built-in component, then no work is needed and we can return.
+            if (BrsBuiltInComponents.has(currentComponent.extends)) {
+                return Promise.resolve();
+            }
+
             let previousComponent = currentComponent;
             currentComponent = this.componentMap.get(currentComponent.extends);
             if (!currentComponent) {


Test patch:
diff --git a/test/brsTypes/components/Group.test.js b/test/brsTypes/components/Group.test.js
new file mode 100644
index 0000000..4c07370
--- /dev/null
+++ b/test/brsTypes/components/Group.test.js
@@ -0,0 +1,34 @@
+const brs = require("brs");
+const { Group } = brs.types;
+
+describe("Group", () => {
+    describe("stringification", () => {
+        it("inits a new Group component", () => {
+            let group = new Group();
+
+            expect(group.toString()).toEqual(
+                `<Component: roSGNode:Group> =
+{
+    change: <UNINITIALIZED>
+    focusable: false
+    focusedchild: invalid
+    id: 
+    visible: true
+    opacity: 1
+    translation: invalid
+    rotation: 0
+    scale: invalid
+    scalerotatecenter: invalid
+    childrenderorder: renderLast
+    inheritparenttransform: true
+    inheritparentopacity: true
+    clippingrect: invalid
+    renderpass: 0
+    muteaudioguide: false
+    enablerendertracking: false
+    rendertracking: disabled
+}`
+            );
+        });
+    });
+});
diff --git a/test/e2e/BrsComponents.test.js b/test/e2e/BrsComponents.test.js
index 958d962..466c4a0 100644
--- a/test/e2e/BrsComponents.test.js
+++ b/test/e2e/BrsComponents.test.js
@@ -374,4 +374,24 @@ describe("end to end brightscript functions", () => {
             "true",
         ]);
     });
+
+    test("components/Group.brs", async () => {
+        outputStreams.root = __dirname + "/resources";
+        await execute([resourceFile("components", "Group.brs")], outputStreams);
+
+        expect(allArgs(outputStreams.stdout.write).filter(arg => arg !== "\n")).toEqual([
+            "group node type:",
+            "Group",
+            "group node visible:",
+            "true",
+            "group node opacity:",
+            "1",
+            "extended group node type:",
+            "ExtendedGroup",
+            "extended group node visible:",
+            "true",
+            "extended group node opacity:",
+            "1",
+        ]);
+    });
 });
diff --git a/test/e2e/BrsMock.test.js b/test/e2e/BrsMock.test.js
index 6b1cc09..77f0659 100644
--- a/test/e2e/BrsMock.test.js
+++ b/test/e2e/BrsMock.test.js
@@ -30,7 +30,7 @@ describe("end to end brightscript functions", () => {
             "create object regex:",
             "roRegex",
             "mock object regex:",
-            "roSGNode",
+            "Node",
             "in name change callback",
             "mock poster name:",
             "poster",
diff --git a/test/e2e/ComponentTop.test.js b/test/e2e/ComponentTop.test.js
new file mode 100644
index 0000000..4649c99
--- /dev/null
+++ b/test/e2e/ComponentTop.test.js
@@ -0,0 +1,37 @@
+const { execute } = require("../../lib");
+const { createMockStreams, resourceFile, allArgs } = require("./E2ETests");
+const lolex = require("lolex");
+
+describe("m.top usage in scenegraph components", () => {
+    let outputStreams;
+    let clock;
+
+    beforeAll(() => {
+        clock = lolex.install({ now: 1547072370937 });
+        outputStreams = createMockStreams();
+    });
+
+    afterEach(() => {
+        jest.resetAllMocks();
+    });
+
+    afterAll(() => {
+        clock.uninstall();
+        jest.restoreAllMocks();
+    });
+
+    test("components/componentTopUsage.brs", async () => {
+        outputStreams.root = __dirname + "/resources";
+        await execute([resourceFile("components", "componentTopUsage.brs")], outputStreams);
+
+        expect(allArgs(outputStreams.stdout.write).filter(arg => arg !== "\n")).toEqual([
+            "this is a default value defined in xml",
+            "true",
+            "false",
+            "300",
+            "100",
+            "this value set using m.top",
+            "invalid",
+        ]);
+    });
+});
diff --git a/test/e2e/resources/components/ExtendedGroup.xml b/test/e2e/resources/components/ExtendedGroup.xml
new file mode 100644
index 0000000..dbbc386
--- /dev/null
+++ b/test/e2e/resources/components/ExtendedGroup.xml
@@ -0,0 +1,7 @@
+
+<?xml version="1.0" encoding="UTF-8"?>
+<component name="ExtendedGroup" extends="Group">
+    <interface>
+        <field id="extendGroupStringField" type="string" value="this extends group" />
+    </interface>
+</component>
diff --git a/test/e2e/resources/components/Group.brs b/test/e2e/resources/components/Group.brs
new file mode 100644
index 0000000..8ef451c
--- /dev/null
+++ b/test/e2e/resources/components/Group.brs
@@ -0,0 +1,11 @@
+sub Main()
+    groupNode = createObject("roSGNode", "Group")
+    print "group node type:" type(groupNode)                                       ' => Group
+    print "group node visible:" groupNode.visible                                  ' => true
+    print "group node opacity:" groupNode.opacity                                  ' => 1
+
+    extendedGroupNode = createObject("roSGNode", "ExtendedGroup")
+    print "extended group node type:" type(extendedGroupNode)                      ' => ExtendedGroup
+    print "extended group node visible:" extendedGroupNode.visible                 ' => true
+    print "extended group node opacity:" extendedGroupNode.opacity                 ' => 1
+end sub
diff --git a/test/e2e/resources/components/Group.xml b/test/e2e/resources/components/Group.xml
deleted file mode 100644
index a6574d7..0000000
--- a/test/e2e/resources/components/Group.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<component name="Group">
-    <interface>
-    </interface>
-    <children>
-    </children>
-</component>
diff --git a/test/e2e/resources/components/MTopWidget.xml b/test/e2e/resources/components/MTopWidget.xml
new file mode 100644
index 0000000..f475392
--- /dev/null
+++ b/test/e2e/resources/components/MTopWidget.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<component name="MTopWidget">
+    <script type="text/brightscript" uri="pkg:/components/scripts/MTopWidget.brs" />
+    <interface>
+        <field id="stringField" type="string" />
+        <field id="defaultValueField" type="string" value="this is a default value defined in xml" />
+    </interface>
+    <children>
+        <Rectangle id="childRectangle" height="100" width="300" />
+    </children>
+</component>
diff --git a/test/e2e/resources/components/componentTopUsage.brs b/test/e2e/resources/components/componentTopUsage.brs
new file mode 100644
index 0000000..e524947
--- /dev/null
+++ b/test/e2e/resources/components/componentTopUsage.brs
@@ -0,0 +1,17 @@
+sub main()
+    ' create an object that uses m.top
+
+    ' MTopWidget will print five values:
+    '   - a default field value defined in xml
+    '   - whether or not m.top is invalid from a local object func call
+    '   - whether or not m.top is invalid from the component itself
+    '   - width value of its child rectangle
+    '   - height value of its child rectangle
+    ' All of these access m.top in some way
+    myComponent = createObject("roSGNode", "MTopWidget")
+
+    print myComponent.stringField ' => "this value set using m.top"
+
+    ' m.top in this script should not refer to anything
+    print m.top ' => invalid
+end sub
diff --git a/test/e2e/resources/components/scripts/MTopWidget.brs b/test/e2e/resources/components/scripts/MTopWidget.brs
new file mode 100644
index 0000000..66bf504
--- /dev/null
+++ b/test/e2e/resources/components/scripts/MTopWidget.brs
@@ -0,0 +1,25 @@
+sub init()
+    m.top.stringField = "this value set using m.top"
+
+    print getDefaultTopValue()
+
+    localObj = { objFunction: isMTopInvalid }
+    print localObj.objFunction()
+    print isMTopInvalid()
+
+    childRect = m.top.findNode("childRectangle")
+    print childRect.width
+    print childRect.height
+end sub
+
+function getDefaultTopValue() as string
+    return m.top.defaultValueField
+end function
+
+function isMTopInvalid() as boolean
+    mTopInvalid = true
+    if m.top <> invalid
+        mTopInvalid = false
+    end if
+    return mTopInvalid
+end function
diff --git a/test/e2e/resources/function/m-pointer-func.brs b/test/e2e/resources/function/m-pointer-func.brs
index a745791..cad9bc4 100644
--- a/test/e2e/resources/function/m-pointer-func.brs
+++ b/test/e2e/resources/function/m-pointer-func.brs
@@ -1,6 +1,6 @@
 Sub Main()
     m.someValue = "root"
-    objA = { myMethod: do_something, someValue: "not root" }    
+    objA = { myMethod: do_something, someValue: "not root" }
     print objA.myMethod()
 End Sub
 
diff --git a/test/extensions/RunInScope.test.js b/test/extensions/RunInScope.test.js
index 617f6fc..8381bae 100644
--- a/test/extensions/RunInScope.test.js
+++ b/test/extensions/RunInScope.test.js
@@ -33,18 +33,22 @@ describe("global Run function", () => {
         );
     });
 
-    it("returns invalid for lexing errors", () => {
+    it("returns invalid for lexing errors", async () => {
         fs.readFileSync.mockImplementation(() => `can't lex this`);
-        expect(RunInScope.call(interpreter, new BrsString("pkg:/errors/lex.brs"))).toBe(
-            BrsInvalid.Instance
-        );
+        try {
+            await RunInScope.call(interpreter, new BrsString("pkg:/errors/lex.brs"));
+        } catch (e) {
+            expect(e).toEqual("Error occurred parsing");
+        }
     });
 
-    it("returns invalid for parse errors", () => {
+    it("returns invalid for parse errors", async () => {
         fs.readFileSync.mockImplementationOnce(() => `if return "parse error" exit while`);
-        expect(RunInScope.call(interpreter, new BrsString("pkg:/errors/parse.brs"))).toBe(
-            BrsInvalid.Instance
-        );
+        try {
+            await RunInScope.call(interpreter, new BrsString("pkg:/errors/parse.brs"));
+        } catch (e) {
+            expect(e).toEqual("Error occurred parsing");
+        }
     });
 
     it("returns invalid for runtime errors", () => {
diff --git a/test/interpreter/Environment.test.js b/test/interpreter/Environment.test.js
index 66e2318..89e27dd 100644
--- a/test/interpreter/Environment.test.js
+++ b/test/interpreter/Environment.test.js
@@ -45,6 +45,13 @@ describe("Environment", () => {
         expect(env.get(identifier("m"))).toBe(newM);
     });
 
+    it("gets and sets root m pointer", () => {
+        let newM = new RoAssociativeArray([{ name: new BrsString("id"), value: new Int32(1738) }]);
+        env.setRootM(newM);
+
+        expect(env.getRootM()).toBe(newM);
+    });
+
     it("gets and sets from Mock scope", () => {
         let val = new BrsString("mockScope");
         env.define(Scope.Mock, "_brs_", val);
diff --git a/test/lexer/Lexer.test.js b/test/lexer/Lexer.test.js
index b4d290c..cb9fcd2 100644
--- a/test/lexer/Lexer.test.js
+++ b/test/lexer/Lexer.test.js
@@ -341,8 +341,16 @@ describe("lexer", () => {
         });
 
         it("reads conditional directives", () => {
-            let { tokens } = Lexer.scan("#if #else if #elseif #else #end if #endif");
+            let { tokens } = Lexer.scan(
+                "#if #else if #elseif #else #end if #endif #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.HashIf,
                 Lexeme.HashElseIf,
                 Lexeme.HashElseIf,