Repository: sjbarag/brs
Pull request #402 (2020-04-30 19:08:27)

Implement m.top for components
- Adds a reference to the node itself on the mPointer as `top`
- Creates a new `mPointer` for each component to prevent `m.top` of created components being accessible everywhere

I can't think of other e2e test cases at the moment since we don't have `callFunc` or `onChange` callbacks working yet.

~~- The tricky part that this needs help on is when visiting calls. Getting the root mPointer prevents function calls inside of components from accessing `m.top`. But only getting the environments mPointer prevents function calls from within objects from accessing the global mPointer.~~
~~- I am not sure how to reconcile the above two scenarios.~~


Patch:
diff --git a/src/brsTypes/components/RoSGNode.ts b/src/brsTypes/components/RoSGNode.ts
index 9081ed4..df1f2e5 100644
--- a/src/brsTypes/components/RoSGNode.ts
+++ b/src/brsTypes/components/RoSGNode.ts
@@ -1109,6 +1109,8 @@ export function createNodeByType(interpreter: Interpreter, type: BrsString) {
             }
 
             interpreter.inSubEnv(subInterpreter => {
+                let mPointer = subInterpreter.environment.getM();
+                mPointer.set(new BrsString("top"), node);
                 if (init instanceof Callable) {
                     init.call(subInterpreter);
                 }
@@ -1127,7 +1129,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/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);


Test patch:
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/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/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,