Repository: sjbarag/brs (js)

Issue:
roAssociativeArray method DoesExist() returning false when the value of the item is invalid
The following code does print `True` on Roku but shows `False` in **brs**
```
a = {b: 1, c: 2, d:invalid}
? a.DoesExist("d")
```

Pull request:
Fixes on roAssociativeArray and roArray
Implements `isEmpty()` for `RoArray` and `RoAssociativeArray` (from `ifEnum` interface), and ensures `doesExist()` returns `true` for explicitly `invalid` properties.

fixes #320 

Patch:
diff --git a/src/brsTypes/Callable.ts b/src/brsTypes/Callable.ts
index d01f059..e4bdff5 100644
--- a/src/brsTypes/Callable.ts
+++ b/src/brsTypes/Callable.ts
@@ -3,6 +3,8 @@ import * as Brs from ".";
 import * as Expr from "../parser/Expression";
 import { Scope } from "../interpreter/Environment";
 import { Location } from "../lexer";
+import { Int32 } from "./Int32";
+import { Float } from "./Float";
 
 /** An argument to a BrightScript `function` or `sub`. */
 export interface Argument {
@@ -255,6 +257,22 @@ export class Callable implements Brs.BrsValue {
                 return;
             }
 
+            if (
+                expected.type.kind === Brs.ValueKind.Float &&
+                received.kind === Brs.ValueKind.Int32
+            ) {
+                args[index] = new Float(received.getValue());
+                return;
+            }
+
+            if (
+                expected.type.kind === Brs.ValueKind.Int32 &&
+                received.kind === Brs.ValueKind.Float
+            ) {
+                args[index] = new Int32(received.getValue());
+                return;
+            }
+
             if (expected.type.kind !== received.kind) {
                 reasons.push({
                     reason: MismatchReason.ArgumentTypeMismatch,
@@ -267,4 +285,35 @@ export class Callable implements Brs.BrsValue {
 
         return reasons;
     }
+
+    /**
+     * Creates several copies of the provided signature and implementation pair, simulating variadic types by creating a
+     * function that accepts zero args, one that accepts one arg, one that accepts two args, (…).
+     *
+     * @param signatureAndImpl the base signature and implementation to make variadic
+     *
+     * @returns an array containing psuedo-variadic versions of the provided signature and implementation
+     */
+    static variadic(signatureAndImpl: SignatureAndImplementation): SignatureAndImplementation[] {
+        let { signature, impl } = signatureAndImpl;
+        return [
+            signatureAndImpl,
+            ...new Array(10).fill(0).map((_, numArgs) => {
+                return {
+                    signature: {
+                        args: [
+                            ...signature.args,
+                            ...new Array(numArgs)
+                                .fill(0)
+                                .map(
+                                    (_, i) => new StdlibArgument(`arg${i}`, Brs.ValueKind.Dynamic)
+                                ),
+                        ],
+                        returns: signature.returns,
+                    },
+                    impl: impl,
+                };
+            }),
+        ];
+    }
 }
diff --git a/src/brsTypes/Int32.ts b/src/brsTypes/Int32.ts
index 5ac9626..2b61ea7 100644
--- a/src/brsTypes/Int32.ts
+++ b/src/brsTypes/Int32.ts
@@ -15,18 +15,18 @@ export class Int32 implements Numeric, Comparable {
 
     /**
      * Creates a new BrightScript 32-bit integer value representing the provided `value`.
-     * @param value the value to store in the BrightScript number, rounded to the nearest 32-bit
+     * @param value the value to store in the BrightScript number, truncated to a 32-bit
      *              integer.
      */
     constructor(initialValue: number) {
-        this.value = Math.round(initialValue);
+        this.value = Math.trunc(initialValue);
     }
 
     /**
      * Creates a new BrightScript 32-bit integer value representing the integer contained in
      * `asString`.
      * @param asString the string representation of the value to store in the BrightScript 32-bit
-     *                 int. Will be rounded to the nearest 32-bit integer.
+     *                 int. Will be truncated to a 32-bit integer.
      * @returns a BrightScript 32-bit integer value representing `asString`.
      */
     static fromString(asString: string): Int32 {
diff --git a/src/brsTypes/Int64.ts b/src/brsTypes/Int64.ts
index 1c7a7c3..f6b4255 100644
--- a/src/brsTypes/Int64.ts
+++ b/src/brsTypes/Int64.ts
@@ -21,7 +21,7 @@ export class Int64 implements Numeric, Comparable {
         if (value instanceof Long) {
             this.value = value;
         } else {
-            this.value = Long.fromNumber(Math.round(value));
+            this.value = Long.fromNumber(Math.trunc(value));
         }
     }
 
@@ -41,14 +41,6 @@ export class Int64 implements Numeric, Comparable {
         }
 
         let i64 = new Int64(Long.fromString(asString, undefined, radix));
-        const decimalLocation = asString.indexOf(".");
-        if (decimalLocation > -1 && decimalLocation + 1 < asString.length) {
-            // Long.fromString truncates to integers instead of rounding, so manually add one to
-            // compensate if necessary
-            if (asString[decimalLocation + 1] >= "5" && asString[decimalLocation + 1] <= "9") {
-                i64 = new Int64(i64.getValue().add(Long.ONE));
-            }
-        }
         return i64;
     }
 
diff --git a/src/brsTypes/components/RoArray.ts b/src/brsTypes/components/RoArray.ts
index 577c5ee..d1d0b16 100644
--- a/src/brsTypes/components/RoArray.ts
+++ b/src/brsTypes/components/RoArray.ts
@@ -27,6 +27,7 @@ export class RoArray extends BrsComponent implements BrsValue, BrsIterable {
             this.sort,
             this.sortBy,
             this.reverse,
+            this.isEmpty,
         ]);
     }
 
@@ -328,4 +329,14 @@ export class RoArray extends BrsComponent implements BrsValue, BrsIterable {
             return BrsInvalid.Instance;
         },
     });
+    // ifEnum
+    private isEmpty = new Callable("isEmpty", {
+        signature: {
+            args: [],
+            returns: ValueKind.Boolean,
+        },
+        impl: (interpreter: Interpreter) => {
+            return BrsBoolean.from(this.elements.length === 0);
+        },
+    });
 }
diff --git a/src/brsTypes/components/RoAssociativeArray.ts b/src/brsTypes/components/RoAssociativeArray.ts
index a114fec..fd7925c 100644
--- a/src/brsTypes/components/RoAssociativeArray.ts
+++ b/src/brsTypes/components/RoAssociativeArray.ts
@@ -34,6 +34,7 @@ export class RoAssociativeArray extends BrsComponent implements BrsValue, BrsIte
             this.keys,
             this.items,
             this.lookup,
+            this.isEmpty,
         ]);
     }
 
@@ -162,7 +163,7 @@ export class RoAssociativeArray extends BrsComponent implements BrsValue, BrsIte
             returns: ValueKind.Boolean,
         },
         impl: (interpreter: Interpreter, str: BrsString) => {
-            return this.get(str) !== BrsInvalid.Instance ? BrsBoolean.True : BrsBoolean.False;
+            return this.elements.has(str.value.toLowerCase()) ? BrsBoolean.True : BrsBoolean.False;
         },
     });
 
@@ -217,4 +218,17 @@ export class RoAssociativeArray extends BrsComponent implements BrsValue, BrsIte
             return this.get(new BrsString(lKey));
         },
     });
+
+    //--------------------------------- ifEnum ---------------------------------
+
+    /** Returns true if enumeration contains no elements, false otherwise	 */
+    private isEmpty = new Callable("isEmpty", {
+        signature: {
+            args: [],
+            returns: ValueKind.Boolean,
+        },
+        impl: (interpreter: Interpreter) => {
+            return BrsBoolean.from(this.elements.size === 0);
+        },
+    });
 }
diff --git a/src/brsTypes/components/Timespan.ts b/src/brsTypes/components/Timespan.ts
index 7e67893..b15d9fd 100644
--- a/src/brsTypes/components/Timespan.ts
+++ b/src/brsTypes/components/Timespan.ts
@@ -64,7 +64,7 @@ export class Timespan extends BrsComponent implements BrsValue {
             returns: ValueKind.Int32,
         },
         impl: (_: Interpreter) => {
-            return new Int32((Date.now() - this.markTime) / 1000);
+            return new Int32(Math.round((Date.now() - this.markTime) / 1000));
         },
     });
 
@@ -82,7 +82,7 @@ export class Timespan extends BrsComponent implements BrsValue {
             let dateToParse = luxon.DateTime.fromISO(date.value, { zone: "utc" });
 
             if (dateToParse.isValid) {
-                dateAsSeconds = (Date.parse(dateToParse.toISO()) - now) / 1000;
+                dateAsSeconds = Math.round((Date.parse(dateToParse.toISO()) - now) / 1000);
             } else {
                 dateAsSeconds = 2077252342;
             }
diff --git a/src/mocks/MockNode.ts b/src/extensions/MockNode.ts
similarity index 100%
rename from src/mocks/MockNode.ts
rename to src/extensions/MockNode.ts
diff --git a/src/extensions/RunInScope.ts b/src/extensions/RunInScope.ts
new file mode 100644
index 0000000..1ac2594
--- /dev/null
+++ b/src/extensions/RunInScope.ts
@@ -0,0 +1,87 @@
+import * as path from "path";
+import * as brs from "../";
+
+import {
+    Callable,
+    StdlibArgument,
+    ValueKind,
+    BrsString,
+    BrsInvalid,
+    BrsType,
+    RoArray,
+    isBrsString,
+} from "../brsTypes";
+import { BrsComponent } from "../brsTypes/components/BrsComponent";
+import { Interpreter } from "../interpreter";
+import { getVolumeByPath, getPath } from "../stdlib/File";
+import { Scope } from "../interpreter/Environment";
+
+/**
+ * Runs a file (or set of files) **in the current global + module scope** with the provided arguments, returning either
+ * the value returned by those files' `main` function or `invalid` if an error occurs.
+ *
+ * @param interpreter the interpreter hosting this call to `Run`
+ * @param filenames a list of files to lex, parse, and run
+ * @param args the arguments to pass into the found `main` function
+ *
+ * @returns the value returned by the executed file(s) if no errors are detected, otherwise `invalid`
+ */
+function runFilesInScope(interpreter: Interpreter, filenames: BrsString[], args: BrsType[]) {
+    let volumes = filenames.map(filename => getVolumeByPath(interpreter, filename.value));
+    let pathsToFiles = filenames.map(filename =>
+        path.join(interpreter.options.root, getPath(filename.value))
+    );
+
+    // if the file-to-run doesn't exist, RBI returns invalid
+    if (!volumes.every(volume => volume != null)) {
+        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;
+    }
+}
+
+export const RunInScope = new Callable(
+    "RunInScope",
+    ...Callable.variadic({
+        signature: {
+            args: [new StdlibArgument("filename", ValueKind.String)],
+            returns: ValueKind.Dynamic,
+        },
+        impl: (interpreter: Interpreter, filename: BrsString, ...args: BrsType[]) => {
+            return runFilesInScope(interpreter, [filename], args);
+        },
+    }),
+    ...Callable.variadic({
+        signature: {
+            args: [new StdlibArgument("filenamearray", ValueKind.Object)],
+            returns: ValueKind.Dynamic,
+        },
+        impl: (interpreter: Interpreter, filenamearray: BrsComponent, ...args: BrsType[]) => {
+            if (
+                filenamearray instanceof RoArray &&
+                filenamearray.getElements().every(isBrsString)
+            ) {
+                return runFilesInScope(
+                    interpreter,
+                    filenamearray.getElements() as BrsString[],
+                    args
+                );
+            }
+
+            // RBI seems to hard-reboot when passed a non-empty associative array, but returns invalid for empty
+            // AA's. Let's return invalid to be safe.
+            return BrsInvalid.Instance;
+        },
+    })
+);
diff --git a/src/extensions/index.ts b/src/extensions/index.ts
new file mode 100644
index 0000000..3862435
--- /dev/null
+++ b/src/extensions/index.ts
@@ -0,0 +1,8 @@
+import { RoAssociativeArray, BrsString } from "../brsTypes";
+import { mockComponent } from "./mockComponent";
+import { RunInScope } from "./RunInScope";
+
+export const _brs_ = new RoAssociativeArray([
+    { name: new BrsString("mockComponent"), value: mockComponent },
+    { name: new BrsString("runInScope"), value: RunInScope },
+]);
diff --git a/src/mocks/mockComponent.ts b/src/extensions/mockComponent.ts
similarity index 70%
rename from src/mocks/mockComponent.ts
rename to src/extensions/mockComponent.ts
index e31686a..9bd019b 100644
--- a/src/mocks/mockComponent.ts
+++ b/src/extensions/mockComponent.ts
@@ -2,16 +2,13 @@ import {
     BrsType,
     ValueKind,
     Callable,
-    BrsString,
     StdlibArgument,
     BrsInvalid,
     RoAssociativeArray,
-    Uninitialized,
 } from "../brsTypes";
-import { MockNode } from "./MockNode";
 import { Interpreter } from "../interpreter";
 
-const mockComponent = new Callable("mockComponent", {
+export const mockComponent = new Callable("mockComponent", {
     signature: {
         args: [
             new StdlibArgument("objToMock", ValueKind.String),
@@ -24,7 +21,3 @@ const mockComponent = new Callable("mockComponent", {
         return BrsInvalid.Instance;
     },
 });
-
-export const _brs_ = new RoAssociativeArray([
-    { name: new BrsString("mockComponent"), value: mockComponent },
-]);
diff --git a/src/index.ts b/src/index.ts
index 4b12984..84c675f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -97,27 +97,20 @@ export async function execute(filenames: string[], options: Partial<ExecutionOpt
 }
 
 /**
- * A synchronous version of `execute`. Executes a BrightScript file by path and writes its output to the streams
- * provided in `options`.
+ * A synchronous version of the lexer-parser flow.
  *
- * @param filename the paths to BrightScript files to execute synchronously
+ * @param filename the paths to BrightScript files to lex and parse synchronously
  * @param options configuration for the execution, including the streams to use for `stdout` and
  *                `stderr` and the base directory for path resolution
- * @param args the set of arguments to pass to the `main` function declared in one of the provided filenames
  *
- * @returns the value returned by the executed file(s)
+ * @returns the AST produced from lexing and parsing the provided files
  */
-export function executeSync(
-    filenames: string[],
-    options: Partial<ExecutionOptions>,
-    args: BrsTypes.BrsType[]
-) {
+export function lexParseSync(filenames: string[], options: Partial<ExecutionOptions>) {
     const executionOptions = Object.assign(defaultExecutionOptions, options);
-    const interpreter = new Interpreter(executionOptions); // shared between files
 
     let manifest = PP.getManifestSync(executionOptions.root);
 
-    let allStatements = filenames
+    return filenames
         .map(filename => {
             let contents = fs.readFileSync(filename, "utf8");
             let scanResults = Lexer.scan(contents, filename);
@@ -126,8 +119,6 @@ export function executeSync(
             return Parser.parse(preprocessorResults.processedTokens).statements;
         })
         .reduce((allStatements, statements) => [...allStatements, ...statements], []);
-
-    return interpreter.exec(allStatements, ...args);
 }
 
 /**
diff --git a/src/interpreter/Environment.ts b/src/interpreter/Environment.ts
index 97a8601..09f90f7 100644
--- a/src/interpreter/Environment.ts
+++ b/src/interpreter/Environment.ts
@@ -92,9 +92,20 @@ export class Environment {
     /**
      * Removes a variable from this environment's function scope.
      * @param name the name of the variable to remove (in the form of an `Identifier`)
+     * @param scope the scope to remove this variable from (defaults to "function")
      */
-    public remove(name: string): void {
-        this.function.delete(name.toLowerCase());
+    public remove(name: string, scope: Scope = Scope.Function): void {
+        let lowercaseName = name.toLowerCase();
+        switch (scope) {
+            case Scope.Module:
+                this.module.delete(lowercaseName);
+                break;
+            case Scope.Function:
+                this.function.delete(lowercaseName);
+                break;
+            default:
+                break;
+        }
     }
 
     /**
@@ -175,8 +186,8 @@ export class Environment {
      */
     public createSubEnvironment(): Environment {
         let newEnvironment = new Environment();
-        newEnvironment.global = this.global;
-        newEnvironment.module = this.module;
+        newEnvironment.global = new Map(this.global);
+        newEnvironment.module = new Map(this.module);
         newEnvironment.mPointer = this.mPointer;
         newEnvironment.mockObjects = this.mockObjects;
         newEnvironment.focusedNode = this.focusedNode;
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index 729a174..240c9db 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -28,7 +28,7 @@ import { Expr, Stmt } from "../parser";
 import { BrsError, TypeMismatch } from "../Error";
 
 import * as StdLib from "../stdlib";
-import { _brs_ } from "../mocks/mockComponent";
+import { _brs_ } from "../extensions";
 
 import { Scope, Environment, NotFound } from "./Environment";
 import { OutputProxy } from "./OutputProxy";
@@ -38,7 +38,6 @@ import { RoAssociativeArray } from "../brsTypes/components/RoAssociativeArray";
 import MemoryFileSystem from "memory-fs";
 import { BrsComponent } from "../brsTypes/components/BrsComponent";
 import { isBoxable, isUnboxable } from "../brsTypes/Boxing";
-import { DottedGet } from "../parser/Expression";
 
 /** The set of options used to configure an interpreter's execution. */
 export interface ExecutionOptions {
diff --git a/src/stdlib/CreateObject.ts b/src/stdlib/CreateObject.ts
index def2946..c4ea72f 100644
--- a/src/stdlib/CreateObject.ts
+++ b/src/stdlib/CreateObject.ts
@@ -8,9 +8,8 @@ import {
     RoAssociativeArray,
 } from "../brsTypes";
 import { BrsObjects } from "../brsTypes/components/BrsObjects";
-import * as Expr from "../parser/Expression";
 import { Interpreter } from "../interpreter";
-import { MockNode } from "../mocks/MockNode";
+import { MockNode } from "../extensions/MockNode";
 
 /** Creates a new instance of a given brightscript component (e.g. roAssociativeArray) */
 export const CreateObject = new Callable("CreateObject", {
diff --git a/src/stdlib/Run.ts b/src/stdlib/Run.ts
index 11ed5ff..0b36d6f 100644
--- a/src/stdlib/Run.ts
+++ b/src/stdlib/Run.ts
@@ -8,7 +8,6 @@ import {
     BrsString,
     BrsInvalid,
     StdlibArgument,
-    SignatureAndImplementation,
     RoArray,
     isBrsString,
 } from "../brsTypes";
@@ -38,8 +37,10 @@ function runFiles(interpreter: Interpreter, filenames: BrsString[], args: BrsTyp
     }
 
     try {
-        let results = brs.executeSync(pathsToFiles, interpreter.options, args);
-        return results[0] || BrsInvalid.Instance;
+        let ast = brs.lexParseSync(pathsToFiles, interpreter.options);
+        // execute the new files in a brand-new interpreter, as no scope is shared with the `Run`-ed files in RBI
+        let sandbox = new Interpreter(interpreter.options);
+        return sandbox.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.
@@ -47,38 +48,9 @@ function runFiles(interpreter: Interpreter, filenames: BrsString[], args: BrsTyp
     }
 }
 
-/**
- * Creates several copies of the provided signature and implementation pair, simulating variadic types by creating a
- * function that accepts zero args, one that accepts one arg, one that accepts two args, (…).
- *
- * @param signatureAndImpl the base signature and implementation to make variadic
- *
- * @returns an array containing psuedo-variadic versions of the provided signature and implementation
- */
-function variadic(signatureAndImpl: SignatureAndImplementation): SignatureAndImplementation[] {
-    let { signature, impl } = signatureAndImpl;
-    return [
-        signatureAndImpl,
-        ...new Array(10).fill(0).map((_, numArgs) => {
-            return {
-                signature: {
-                    args: [
-                        ...signature.args,
-                        ...new Array(numArgs)
-                            .fill(0)
-                            .map((_, i) => new StdlibArgument(`arg${i}`, ValueKind.Dynamic)),
-                    ],
-                    returns: signature.returns,
-                },
-                impl: impl,
-            };
-        }),
-    ];
-}
-
 export const Run = new Callable(
     "Run",
-    ...variadic({
+    ...Callable.variadic({
         signature: {
             args: [new StdlibArgument("filename", ValueKind.String)],
             returns: ValueKind.Dynamic,
@@ -87,7 +59,7 @@ export const Run = new Callable(
             return runFiles(interpreter, [filename], args);
         },
     }),
-    ...variadic({
+    ...Callable.variadic({
         signature: {
             args: [new StdlibArgument("filenamearray", ValueKind.Object)],
             returns: ValueKind.Dynamic,
diff --git a/src/stdlib/index.ts b/src/stdlib/index.ts
index c9515f0..cdf76dd 100644
--- a/src/stdlib/index.ts
+++ b/src/stdlib/index.ts
@@ -22,6 +22,6 @@ export * from "./File";
 export * from "./Json";
 export * from "./Math";
 export * from "./Print";
-export * from "./Run";
+export { Run } from "./Run";
 export * from "./String";
 export * from "./Type";


Test patch:
diff --git a/test/brsTypes/Callable.test.js b/test/brsTypes/Callable.test.js
index 293327f..c620501 100644
--- a/test/brsTypes/Callable.test.js
+++ b/test/brsTypes/Callable.test.js
@@ -129,6 +129,38 @@ describe("Callable", () => {
             });
         });
 
+        it("allows float to be passed to an integer argument", () => {
+            const hasArgs = new BrsTypes.Callable("acceptsAnything", {
+                signature: {
+                    args: [new BrsTypes.StdlibArgument("intArg", BrsTypes.ValueKind.Int32)],
+                    returns: BrsTypes.String,
+                },
+                impl: () => {},
+            });
+
+            expect(
+                hasArgs
+                    .getAllSignatureMismatches([new BrsTypes.Float(1.5)])
+                    .map(mm => mm.mismatches)[0]
+            ).toEqual([]);
+        });
+
+        it("allows integer to be passed to a float argument", () => {
+            const hasArgs = new BrsTypes.Callable("acceptsAnything", {
+                signature: {
+                    args: [new BrsTypes.StdlibArgument("floatArg", BrsTypes.ValueKind.Float)],
+                    returns: BrsTypes.String,
+                },
+                impl: () => {},
+            });
+
+            expect(
+                hasArgs
+                    .getAllSignatureMismatches([new BrsTypes.Int32(4)])
+                    .map(mm => mm.mismatches)[0]
+            ).toEqual([]);
+        });
+
         it("allows any type for dynamic and object", () => {
             const hasArgs = new BrsTypes.Callable("acceptsAnything", {
                 signature: {
diff --git a/test/brsTypes/Int32.test.js b/test/brsTypes/Int32.test.js
index 1cce9e0..e549930 100644
--- a/test/brsTypes/Int32.test.js
+++ b/test/brsTypes/Int32.test.js
@@ -10,18 +10,18 @@ const {
 } = require("../../lib/brsTypes");
 
 describe("Int32", () => {
-    it("rounds initial values to integers", () => {
+    it("truncates initial values to integers", () => {
         let three = new Int32(3.4);
         let four = new Int32(3.5);
         expect(three.getValue()).toBe(3);
-        expect(four.getValue()).toBe(4);
+        expect(four.getValue()).toBe(3);
     });
 
     it("creates base-10 integers from strings", () => {
         let three = Int32.fromString("3.4");
         let four = Int32.fromString("3.5");
         expect(three.getValue()).toBe(3);
-        expect(four.getValue()).toBe(4);
+        expect(four.getValue()).toBe(3);
     });
 
     it("creates base-16 integers from strings", () => {
diff --git a/test/brsTypes/Int64.test.js b/test/brsTypes/Int64.test.js
index e95822a..d01a95d 100644
--- a/test/brsTypes/Int64.test.js
+++ b/test/brsTypes/Int64.test.js
@@ -11,18 +11,14 @@ const {
 const Long = require("long");
 
 describe("Int64", () => {
-    it("rounds initial values to integers", () => {
-        let three = new Int64(3.4);
-        let four = new Int64(3.5);
-        expect(three.getValue().toNumber()).toBe(3);
-        expect(four.getValue().toNumber()).toBe(4);
+    it("truncates initial values to integers", () => {
+        let threePointNine = new Int64(3.9);
+        expect(threePointNine.getValue().toNumber()).toBe(3);
     });
 
     it("creates base-10 integers from strings", () => {
-        let three = Int64.fromString("3.4");
-        let four = Int64.fromString("3.5");
-        expect(three.getValue().toNumber()).toBe(3);
-        expect(four.getValue().toNumber()).toBe(4);
+        let threePointNine = Int64.fromString("3.9");
+        expect(threePointNine.getValue().toNumber()).toBe(3);
     });
 
     it("creates base-16 integers from strings", () => {
diff --git a/test/brsTypes/components/RoAssociativeArray.test.js b/test/brsTypes/components/RoAssociativeArray.test.js
index d64a378..0cf6b72 100644
--- a/test/brsTypes/components/RoAssociativeArray.test.js
+++ b/test/brsTypes/components/RoAssociativeArray.test.js
@@ -194,6 +194,16 @@ describe("RoAssociativeArray", () => {
                 expect(result.kind).toBe(ValueKind.Boolean);
                 expect(result).toBe(BrsBoolean.False);
             });
+            it("returns true even when the value of an item contains invalid in the array", () => {
+                let aa = new RoAssociativeArray([
+                    { name: new BrsString("letter1"), value: BrsInvalid.Instance },
+                ]);
+
+                let doesexist = aa.getMethod("doesexist");
+                let result = doesexist.call(interpreter, new BrsString("letter1"));
+                expect(result.kind).toBe(ValueKind.Boolean);
+                expect(result).toBe(BrsBoolean.True);
+            });
         });
 
         describe("append", () => {
diff --git a/test/e2e/resources/components/roArray.brs b/test/e2e/resources/components/roArray.brs
index e4fbd0f..e901fa1 100644
--- a/test/e2e/resources/components/roArray.brs
+++ b/test/e2e/resources/components/roArray.brs
@@ -10,5 +10,5 @@ sub Main()
     print "can delete elements: " arr.delete(1) ' => true
 
     arr.clear()
-    print "can empty itself: " arr.count() = 0  ' => true
+    print "can empty itself: " arr.isEmpty()    ' => true
 end sub
diff --git a/test/e2e/resources/components/roAssociativeArray.brs b/test/e2e/resources/components/roAssociativeArray.brs
index e801b34..c12dab9 100644
--- a/test/e2e/resources/components/roAssociativeArray.brs
+++ b/test/e2e/resources/components/roAssociativeArray.brs
@@ -14,5 +14,5 @@ sub main()
     print "can check for existence: " aa.doesExist("bar")       ' => true
 
     aa.clear()
-    print "can empty itself: " aa.count() = 0                   ' => true
+    print "can empty itself: " aa.isEmpty()                     ' => true
 end sub
diff --git a/test/mocks/MockNode.test.js b/test/extensions/MockNode.test.js
similarity index 94%
rename from test/mocks/MockNode.test.js
rename to test/extensions/MockNode.test.js
index 5961ea1..6e0c3fd 100644
--- a/test/mocks/MockNode.test.js
+++ b/test/extensions/MockNode.test.js
@@ -1,7 +1,7 @@
 const brs = require("brs");
 const { ValueKind, Callable, BrsString, RoAssociativeArray } = brs.types;
 const { Interpreter } = require("../../lib/interpreter");
-const { MockNode } = require("../../lib/mocks/MockNode");
+const { MockNode } = require("../../lib/extensions/MockNode");
 
 describe("MockNode", () => {
     describe("methods", () => {
diff --git a/test/extensions/RunInScope.test.js b/test/extensions/RunInScope.test.js
new file mode 100644
index 0000000..617f6fc
--- /dev/null
+++ b/test/extensions/RunInScope.test.js
@@ -0,0 +1,119 @@
+const brs = require("brs");
+const { Callable, BrsString, BrsInvalid, RoArray, RoAssociativeArray, ValueKind } = brs.types;
+const { RunInScope } = require("../../lib/extensions/RunInScope");
+const { Interpreter } = require("../../lib/interpreter");
+const { Scope } = require("../../lib/interpreter/Environment");
+const fs = require("fs");
+
+jest.mock("fs");
+
+describe("global Run function", () => {
+    let interpreter;
+
+    beforeEach(() => {
+        interpreter = new Interpreter();
+    });
+
+    afterEach(() => {
+        fs.readFileSync.mockRestore();
+    });
+
+    it("returns invalid for unrecognized devices", () => {
+        expect(RunInScope.call(interpreter, new BrsString("notADevice:/etc/hosts"))).toBe(
+            BrsInvalid.Instance
+        );
+    });
+
+    it("returns invalid for unreadable files", () => {
+        fs.readFileSync.mockImplementation(() => {
+            throw new Error("file not found");
+        });
+        expect(RunInScope.call(interpreter, new BrsString("notADevice:/etc/hosts"))).toBe(
+            BrsInvalid.Instance
+        );
+    });
+
+    it("returns invalid for lexing errors", () => {
+        fs.readFileSync.mockImplementation(() => `can't lex this`);
+        expect(RunInScope.call(interpreter, new BrsString("pkg:/errors/lex.brs"))).toBe(
+            BrsInvalid.Instance
+        );
+    });
+
+    it("returns invalid for parse errors", () => {
+        fs.readFileSync.mockImplementationOnce(() => `if return "parse error" exit while`);
+        expect(RunInScope.call(interpreter, new BrsString("pkg:/errors/parse.brs"))).toBe(
+            BrsInvalid.Instance
+        );
+    });
+
+    it("returns invalid for runtime errors", () => {
+        fs.readFileSync.mockImplementationOnce(() => `sub main(): _ = {}: _.crash(): end sub`);
+        expect(RunInScope.call(interpreter, new BrsString("pkg:/errors/exec.brs"))).toBe(
+            BrsInvalid.Instance
+        );
+    });
+
+    it("returns invalid when provided a not-array component", () => {
+        expect(RunInScope.call(interpreter, new RoAssociativeArray([]))).toBe(BrsInvalid.Instance);
+    });
+
+    it("returns invalid when provided an empty array of files", () => {
+        expect(RunInScope.call(interpreter, new RoArray([]))).toBe(BrsInvalid.Instance);
+    });
+
+    it("returns invalid when provided an array with non-strings", () => {
+        expect(RunInScope.call(interpreter, new RoArray([BrsInvalid.Instance]))).toBe(
+            BrsInvalid.Instance
+        );
+    });
+
+    it("allows main to be redefined", () => {
+        interpreter.environment.define(
+            Scope.Module,
+            "main",
+            new Callable("main", {
+                signature: {
+                    args: [],
+                    returns: ValueKind.Void,
+                },
+                impl: interpreter => {
+                    // just an empty main function - it just needs to exist for this test
+                    return BrsInvalid.Instance;
+                },
+            })
+        );
+
+        fs.readFileSync.mockImplementationOnce(
+            () => `function main(): return "hello!": end function`
+        );
+
+        expect(RunInScope.call(interpreter, new BrsString("pkg:/success/exec.brs"))).toEqual(
+            new BrsString("hello!")
+        );
+    });
+
+    it("can call functions declared in original scope", () => {
+        interpreter.environment.define(
+            Scope.Module,
+            "greet",
+            new Callable("greet", {
+                signature: {
+                    args: [],
+                    returns: ValueKind.String,
+                },
+                impl: interpreter => {
+                    return new BrsString("hello!");
+                },
+            })
+        );
+
+        fs.readFileSync.mockImplementationOnce(
+            () => `function main(): return greet(): end function`
+        );
+
+        expect(RunInScope.call(interpreter, new BrsString("pkg:/success/exec.brs"))).toEqual(
+            new BrsString("hello!")
+        );
+    });
+});