Repository: sjbarag/brs
Pull request #395 (2020-04-09 20:17:06)

Allow invalid returns for signatures that return object
It turns out there is a `RoInvalid` type when you return `invalid` as object or even call `createObject("RoInvalid")`

This PR implements that type and adds unit tests and end to end tests covering these scenarios.

[RoInvalid](https://developer.roku.com/docs/references/brightscript/components/roinvalid.md)
fixes https://github.com/sjbarag/brs/issues/313

underwoo16: I'm actually going to check the brightscript reference on device to see if it will return `RoInvalid` 

Patch:
diff --git a/src/brsTypes/BrsType.ts b/src/brsTypes/BrsType.ts
index 39ab684..09b57b3 100644
--- a/src/brsTypes/BrsType.ts
+++ b/src/brsTypes/BrsType.ts
@@ -4,6 +4,7 @@ import { RoString } from "./components/RoString";
 import { Int32 } from "./Int32";
 import { Float } from "./Float";
 import { roBoolean } from "./components/RoBoolean";
+import { roInvalid } from "./components/RoInvalid";
 
 /** Set of values supported in BrightScript. */
 export enum ValueKind {
@@ -299,7 +300,7 @@ export class BrsBoolean implements BrsValue, Comparable, Boxable {
 }
 
 /** Internal representation of the BrightScript `invalid` value. */
-export class BrsInvalid implements BrsValue, Comparable {
+export class BrsInvalid implements BrsValue, Comparable, Boxable {
     readonly kind = ValueKind.Invalid;
     static Instance = new BrsInvalid();
 
@@ -325,6 +326,10 @@ export class BrsInvalid implements BrsValue, Comparable {
     toString(parent?: BrsType) {
         return "invalid";
     }
+
+    box() {
+        return new roInvalid();
+    }
 }
 
 /** Internal representation of uninitialized BrightScript variables. */
diff --git a/src/brsTypes/components/BrsObjects.ts b/src/brsTypes/components/BrsObjects.ts
index 581f0de..f4f60f6 100644
--- a/src/brsTypes/components/BrsObjects.ts
+++ b/src/brsTypes/components/BrsObjects.ts
@@ -14,6 +14,7 @@ import { Double } from "../Double";
 import { Float } from "../Float";
 import { Int32 } from "../Int32";
 import { Interpreter } from "../../interpreter";
+import { roInvalid } from "./RoInvalid";
 
 /** Map containing a list of brightscript components that can be created. */
 export const BrsObjects = new Map<string, Function>([
@@ -34,4 +35,5 @@ export const BrsObjects = new Map<string, Function>([
     ["rodouble", (_: Interpreter, literal: Double) => new roDouble(literal)],
     ["rofloat", (_: Interpreter, literal: Float) => new roFloat(literal)],
     ["roint", (_: Interpreter, literal: Int32) => new roInt(literal)],
+    ["roinvalid", (_: Interpreter) => new roInvalid()],
 ]);
diff --git a/src/brsTypes/components/RoInvalid.ts b/src/brsTypes/components/RoInvalid.ts
new file mode 100644
index 0000000..50ecdd1
--- /dev/null
+++ b/src/brsTypes/components/RoInvalid.ts
@@ -0,0 +1,56 @@
+import { BrsComponent } from "./BrsComponent";
+import { BrsValue, ValueKind, BrsString, BrsBoolean, BrsInvalid } from "../BrsType";
+import { Callable } from "../Callable";
+import { Interpreter } from "../../interpreter";
+import { BrsType } from "..";
+import { Unboxable } from "../Boxing";
+
+export class roInvalid extends BrsComponent implements BrsValue, Unboxable {
+    readonly kind = ValueKind.Object;
+    private intrinsic: BrsInvalid;
+
+    public getValue(): BrsInvalid {
+        return this.intrinsic;
+    }
+
+    constructor() {
+        super("roInvalid");
+
+        this.intrinsic = BrsInvalid.Instance;
+        this.registerMethods({
+            ifToStr: [this.toStr],
+        });
+    }
+
+    unbox() {
+        return this.intrinsic;
+    }
+
+    equalTo(other: BrsType): BrsBoolean {
+        if (other instanceof BrsInvalid) {
+            return BrsBoolean.True;
+        }
+
+        if (other instanceof roInvalid) {
+            return BrsBoolean.True;
+        }
+
+        return BrsBoolean.False;
+    }
+
+    toString(_parent?: BrsType): string {
+        return "<Component: roInvalid>";
+    }
+
+    // ---------- ifToStr ----------
+
+    private toStr = new Callable("toStr", {
+        signature: {
+            args: [],
+            returns: ValueKind.String,
+        },
+        impl: (_interpreter: Interpreter) => {
+            return new BrsString(this.intrinsic.toString());
+        },
+    });
+}
diff --git a/src/brsTypes/index.ts b/src/brsTypes/index.ts
index 2a28b66..f45d626 100644
--- a/src/brsTypes/index.ts
+++ b/src/brsTypes/index.ts
@@ -36,6 +36,7 @@ export * from "./components/RoBoolean";
 export * from "./components/RoDouble";
 export * from "./components/RoFloat";
 export * from "./components/RoInt";
+export * from "./components/RoInvalid";
 export * from "./Callable";
 
 /**


Test patch:
diff --git a/test/brsTypes/components/RoInvalid.test.js b/test/brsTypes/components/RoInvalid.test.js
new file mode 100644
index 0000000..f09d045
--- /dev/null
+++ b/test/brsTypes/components/RoInvalid.test.js
@@ -0,0 +1,34 @@
+const brs = require("brs");
+const { RoAssociativeArray, BrsBoolean, BrsInvalid, roInvalid } = brs.types;
+
+describe("RoInvalid", () => {
+    describe("comparisons", () => {
+        it("is equal to itself", () => {
+            let a = new roInvalid();
+            expect(a.equalTo(a)).toBe(BrsBoolean.True);
+        });
+
+        it("is equal to invalid", () => {
+            let a = new roInvalid();
+            expect(a.equalTo(BrsInvalid.Instance)).toBe(BrsBoolean.True);
+        });
+
+        it("is equal to roInvalid", () => {
+            let a = new roInvalid();
+            expect(a.equalTo(new roInvalid())).toBe(BrsBoolean.True);
+        });
+
+        it("is not equal to a RoAssocArray", () => {
+            let a = new roInvalid();
+            let b = new RoAssociativeArray([]);
+            expect(a.equalTo(b)).toBe(BrsBoolean.False);
+        });
+    });
+
+    describe("stringification", () => {
+        it("stringifies itself", () => {
+            let a = new roInvalid();
+            expect(a.toString()).toBe("<Component: roInvalid>");
+        });
+    });
+});
diff --git a/test/e2e/BrsComponents.test.js b/test/e2e/BrsComponents.test.js
index c25acdb..0f903c9 100644
--- a/test/e2e/BrsComponents.test.js
+++ b/test/e2e/BrsComponents.test.js
@@ -356,4 +356,16 @@ describe("end to end brightscript functions", () => {
             "23",
         ]);
     });
+
+    test("components/roInvalid.brs", async () => {
+        await execute([resourceFile("components", "roInvalid.brs")], outputStreams);
+
+        expect(allArgs(outputStreams.stderr.write)).toEqual([]);
+        expect(allArgs(outputStreams.stdout.write).filter(arg => arg !== "\n")).toEqual([
+            "roInvalid",
+            "<Component: roInvalid>",
+            "invalid",
+            "true",
+        ]);
+    });
 });
diff --git a/test/e2e/Functions.test.js b/test/e2e/Functions.test.js
index be7272d..42e9112 100644
--- a/test/e2e/Functions.test.js
+++ b/test/e2e/Functions.test.js
@@ -52,6 +52,9 @@ describe("end to end functions", () => {
             "boxedReturnType:",
             "roFloat",
             "3.14159",
+            "invalidAsObject:",
+            "roInvalid",
+            "<Component: roInvalid>",
         ]);
     });
 
diff --git a/test/e2e/resources/components/roInvalid.brs b/test/e2e/resources/components/roInvalid.brs
new file mode 100644
index 0000000..1d4a6c6
--- /dev/null
+++ b/test/e2e/resources/components/roInvalid.brs
@@ -0,0 +1,9 @@
+sub main()
+    r = createObject("RoInvalid")
+    print type(r)
+    print r
+    print r.toStr()
+
+    result = (r = invalid)
+    print result
+end sub
diff --git a/test/e2e/resources/function/return.brs b/test/e2e/resources/function/return.brs
index 5d9a44f..93a34fb 100644
--- a/test/e2e/resources/function/return.brs
+++ b/test/e2e/resources/function/return.brs
@@ -32,6 +32,10 @@ function boxedReturnType() as object
     return 3.14159
 end function
 
+function returnInvalidAsObject() as object
+    return invalid
+end function
+
 ' ----- then execute them -----
 print staticReturn()
 print conditionalReturn(true)
@@ -41,4 +45,8 @@ print whileLoopReturn(3)
 
 boxed = boxedReturnType()
 print "boxedReturnType:"
-print type(boxed) boxed ' => RoFloat 3.14159
\ No newline at end of file
+print type(boxed) boxed ' => RoFloat 3.14159
+
+print "invalidAsObject:"
+boxedInvalid = returnInvalidAsObject() ' => invalid
+print type(boxedInvalid) boxedInvalid ' => roInvalid <Component: roInvalid>
diff --git a/test/interpreter/Call.test.js b/test/interpreter/Call.test.js
index 20c9a71..b5dfa9a 100644
--- a/test/interpreter/Call.test.js
+++ b/test/interpreter/Call.test.js
@@ -3,7 +3,7 @@ const Stmt = require("../../lib/parser/Statement");
 const { Interpreter } = require("../../lib/interpreter");
 const brs = require("brs");
 const { Lexeme } = brs.lexer;
-const { BrsString, Int32, roInt, ValueKind } = brs.types;
+const { BrsString, Int32, roInt, ValueKind, BrsInvalid, roInvalid } = brs.types;
 
 const { token, identifier } = require("../parser/ParserTests");
 
@@ -189,6 +189,42 @@ describe("interpreter calls", () => {
         );
     });
 
+    it("boxes invalid when return type is Object", () => {
+        const ast = [
+            new Stmt.Function(
+                identifier("foo"),
+                new Expr.Function(
+                    [],
+                    ValueKind.Object,
+                    new Stmt.Block(
+                        [
+                            new Stmt.Return(
+                                { return: token(Lexeme.Return, "return") },
+                                new Expr.Literal(BrsInvalid.Instance)
+                            ),
+                        ],
+                        token(Lexeme.Newline, "\n")
+                    )
+                )
+            ),
+            new Stmt.Assignment(
+                { equals: token(Lexeme.Equals, "=") },
+                identifier("result"),
+                new Stmt.Expression(
+                    new Expr.Call(
+                        new Expr.Variable(identifier("foo")),
+                        token(Lexeme.RightParen, ")"),
+                        [] // no args required
+                    )
+                )
+            ),
+        ];
+        interpreter.exec(ast);
+        let result = interpreter.environment.get(identifier("result"));
+        expect(result.kind).toEqual(ValueKind.Object);
+        expect(result.value).toEqual(new roInvalid().value);
+    });
+
     it("errors when returning from a void return", () => {
         const ast = [
             new Stmt.Function(