Repository: sjbarag/brs
Pull request #254 (2019-06-07 00:58:22)

feat(stdlib,interp): Implement autoboxing and roString (ifStringOps only)
`roString` — specifically the methods described in `ifStringOps` — is one of the most often-used non-SceneGraph types in the BrightScript language.  It's not necessarily directly created by BrightScript authors, but it gets created whenever `foo.split("/")` or `bar.encodeUriComponent()` are called due to the reference implementation's automatic boxing rules.  Basically, if you use a boxable primitive like an object, it'll get automatically boxed for you, and the temporary boxed value will be destroyed at the end of the operation.

This PR implements the `ifStringOps` functions on a new `roString` type, and includes that automatic boxing behavior.

sjbarag: Ignore the commit history; it'll get squashed

Patch:
diff --git a/src/brsTypes/Boxing.ts b/src/brsTypes/Boxing.ts
new file mode 100644
index 0000000..f90d610
--- /dev/null
+++ b/src/brsTypes/Boxing.ts
@@ -0,0 +1,19 @@
+import { BrsComponent } from "./components/BrsComponent";
+import { BrsValue, BrsType, ValueKind } from "./";
+import { RoString } from "./components/RoString";
+
+export interface Boxable {
+    box(): BrsComponent;
+}
+
+export interface Unboxable {
+    unbox(): BrsValue;
+}
+
+export function isBoxable(value: BrsType): value is BrsType & Boxable {
+    return "box" in value;
+}
+
+export function isUnboxable(value: BrsType): value is BrsType & Unboxable {
+    return "unbox" in value;
+}
diff --git a/src/brsTypes/BrsType.ts b/src/brsTypes/BrsType.ts
index 740941d..cb04274 100644
--- a/src/brsTypes/BrsType.ts
+++ b/src/brsTypes/BrsType.ts
@@ -1,4 +1,6 @@
 import { BrsType } from ".";
+import { Boxable } from "./Boxing";
+import { RoString } from "./components/RoString";
 
 /** Set of values supported in BrightScript. */
 export enum ValueKind {
@@ -101,6 +103,13 @@ export interface BrsValue {
      * @returns A human-readable representation of this value.
      */
     toString(parent?: BrsType): string;
+
+    /**
+     * Determines whether or not this value is equal to some `other` value.
+     * @param other The value to compare this value to.
+     * @returns `true` if this value is strictly equal to the `other` value, otherwise `false`.
+     */
+    equalTo(other: BrsType): BrsBoolean;
 }
 
 /** The set of operations required for a BrightScript datatype to be compared to another. */
@@ -118,37 +127,38 @@ export interface Comparable {
      * @returns `true` if this value is greater than the `other` value, otherwise `false`.
      */
     greaterThan(other: BrsType): BrsBoolean;
-
-    /**
-     * Determines whether or not this value is equal to some `other` value.
-     * @param other The value to compare this value to.
-     * @returns `true` if this value is strictly equal to the `other` value, otherwise `false`.
-     */
-    equalTo(other: BrsType): BrsBoolean;
 }
 
 /** Internal representation of a string in BrightScript. */
-export class BrsString implements BrsValue, Comparable {
+export class BrsString implements BrsValue, Comparable, Boxable {
     readonly kind = ValueKind.String;
     constructor(readonly value: string) {}
 
     lessThan(other: BrsType): BrsBoolean {
         if (other.kind === ValueKind.String) {
             return BrsBoolean.from(this.value < other.value);
+        } else if (other instanceof RoString) {
+            return this.lessThan(other.unbox());
         }
+
         return BrsBoolean.False;
     }
 
     greaterThan(other: BrsType): BrsBoolean {
         if (other.kind === ValueKind.String) {
             return BrsBoolean.from(this.value > other.value);
+        } else if (other instanceof RoString) {
+            return this.greaterThan(other.unbox());
         }
+
         return BrsBoolean.False;
     }
 
     equalTo(other: BrsType): BrsBoolean {
         if (other.kind === ValueKind.String) {
             return BrsBoolean.from(this.value === other.value);
+        } else if (other instanceof RoString) {
+            return this.equalTo(other.unbox());
         }
         return BrsBoolean.False;
     }
@@ -160,6 +170,10 @@ export class BrsString implements BrsValue, Comparable {
     concat(other: BrsString) {
         return new BrsString(this.value + other.value);
     }
+
+    box() {
+        return new RoString(this);
+    }
 }
 
 /** Internal representation of a boolean in BrightScript. */
diff --git a/src/brsTypes/components/BrsComponent.ts b/src/brsTypes/components/BrsComponent.ts
index 2b12647..cf0707e 100644
--- a/src/brsTypes/components/BrsComponent.ts
+++ b/src/brsTypes/components/BrsComponent.ts
@@ -6,8 +6,11 @@ export class BrsComponent {
     private methods: Map<string, Callable> = new Map();
     private readonly componentName: string;
 
-    constructor(name: string) {
+    readonly interfaces: Set<string>;
+
+    constructor(name: string, interfaces: string[] = []) {
         this.componentName = name;
+        this.interfaces = new Set(interfaces);
     }
 
     /**
@@ -19,7 +22,9 @@ export class BrsComponent {
     }
 
     protected registerMethods(methods: Callable[]) {
-        this.methods = new Map(methods.map(m => [m.name!, m] as [string, Callable]));
+        this.methods = new Map(
+            methods.map(m => [(m.name || "").toLowerCase(), m] as [string, Callable])
+        );
     }
 
     getMethod(index: string): Callable | undefined {
diff --git a/src/brsTypes/components/BrsObjects.ts b/src/brsTypes/components/BrsObjects.ts
index 47aade7..fe02289 100644
--- a/src/brsTypes/components/BrsObjects.ts
+++ b/src/brsTypes/components/BrsObjects.ts
@@ -3,6 +3,7 @@ import { RoArray } from "./RoArray";
 import { Timespan } from "./Timespan";
 import { RoRegex } from "./RoRegex";
 import { BrsString } from "../BrsType";
+import { RoString } from "./RoString";
 
 /** Map containing a list of brightscript components that can be created. */
 export const BrsObjects = new Map<string, Function>([
@@ -10,4 +11,5 @@ export const BrsObjects = new Map<string, Function>([
     ["roarray", () => new RoArray([])],
     ["rotimespan", () => new Timespan()],
     ["roregex", (expression: BrsString, flags: BrsString) => new RoRegex(expression, flags)],
+    ["rostring", (literal: BrsString) => new RoString(literal)],
 ]);
diff --git a/src/brsTypes/components/RoArray.ts b/src/brsTypes/components/RoArray.ts
index a8115a3..2a3fbb2 100644
--- a/src/brsTypes/components/RoArray.ts
+++ b/src/brsTypes/components/RoArray.ts
@@ -38,14 +38,6 @@ export class RoArray extends BrsComponent implements BrsValue, BrsIterable {
         ].join("\n");
     }
 
-    lessThan(other: BrsType) {
-        return BrsBoolean.False;
-    }
-
-    greaterThan(other: BrsType) {
-        return BrsBoolean.False;
-    }
-
     equalTo(other: BrsType) {
         return BrsBoolean.False;
     }
diff --git a/src/brsTypes/components/RoAssociativeArray.ts b/src/brsTypes/components/RoAssociativeArray.ts
index ba7bed5..a5c69d3 100644
--- a/src/brsTypes/components/RoAssociativeArray.ts
+++ b/src/brsTypes/components/RoAssociativeArray.ts
@@ -52,14 +52,6 @@ export class RoAssociativeArray extends BrsComponent implements BrsValue, BrsIte
         ].join("\n");
     }
 
-    lessThan(other: BrsType) {
-        return BrsBoolean.False;
-    }
-
-    greaterThan(other: BrsType) {
-        return BrsBoolean.False;
-    }
-
     equalTo(other: BrsType) {
         return BrsBoolean.False;
     }
diff --git a/src/brsTypes/components/RoRegex.ts b/src/brsTypes/components/RoRegex.ts
index aaf384a..e963d8b 100644
--- a/src/brsTypes/components/RoRegex.ts
+++ b/src/brsTypes/components/RoRegex.ts
@@ -1,4 +1,4 @@
-import { BrsBoolean, BrsInvalid, BrsString, BrsValue, ValueKind } from "../BrsType";
+import { BrsBoolean, BrsString, BrsValue, ValueKind } from "../BrsType";
 import { BrsComponent } from "./BrsComponent";
 import { BrsType } from "..";
 import { Callable, StdlibArgument } from "../Callable";
@@ -24,10 +24,14 @@ export class RoRegex extends BrsComponent implements BrsValue {
         ]);
     }
 
-    toString(parent?: BrsType): string {
+    toString(parent?: BrsType) {
         return "<Component: roRegex>";
     }
 
+    equalTo(other: BrsType) {
+        return BrsBoolean.False;
+    }
+
     /**
      * Checks and parses the flags to avoid passing flags
      * that are not supported
@@ -82,9 +86,9 @@ export class RoRegex extends BrsComponent implements BrsValue {
         },
         impl: (interpreter: Interpreter, str: BrsString) => {
             const result = this.jsRegex.exec(str.value);
-            let arr: (BrsString | BrsInvalid)[] = [];
+            let arr: BrsString[] = [];
             if (result !== null) {
-                arr = result.map(match => (match ? new BrsString(match) : BrsInvalid.Instance));
+                arr = result.map(match => new BrsString(match || ""));
             }
 
             return new RoArray(arr);
@@ -153,7 +157,7 @@ export class RoRegex extends BrsComponent implements BrsValue {
             let matches: string[] | null;
 
             while ((matches = this.jsRegex.exec(str.value)) !== null) {
-                let item = matches[0] ? new BrsString(matches[0]) : BrsInvalid.Instance;
+                let item = new BrsString(matches[0] || "");
                 arr.push(new RoArray([item]));
             }
             return new RoArray(arr);
diff --git a/src/brsTypes/components/RoString.ts b/src/brsTypes/components/RoString.ts
new file mode 100644
index 0000000..e3fcd49
--- /dev/null
+++ b/src/brsTypes/components/RoString.ts
@@ -0,0 +1,439 @@
+import { BrsComponent } from "./BrsComponent";
+import { RoArray } from "./RoArray";
+import { BrsValue, ValueKind, BrsString, BrsBoolean, BrsInvalid, Comparable } from "../BrsType";
+import { Callable, StdlibArgument } from "../Callable";
+import { Interpreter } from "../../interpreter";
+import { BrsType } from "..";
+import { Unboxable } from "../Boxing";
+import { Int32 } from "../Int32";
+import { Float } from "../Float";
+
+export class RoString extends BrsComponent implements BrsValue, Comparable, Unboxable {
+    readonly kind = ValueKind.Object;
+    private intrinsic: BrsString;
+
+    public getValue(): string {
+        return this.intrinsic.value;
+    }
+
+    constructor(initialValue: BrsString) {
+        super("roString", ["ifStringOps"]);
+
+        this.intrinsic = initialValue;
+        this.registerMethods([
+            this.setString,
+            this.appendString,
+            this.len,
+            this.left,
+            this.right,
+            this.mid,
+            this.instr,
+            this.replace,
+            this.trim,
+            this.toInt,
+            this.toFloat,
+            this.tokenize,
+            this.split,
+            this.getEntityEncode,
+            this.escape,
+            this.unescape,
+            this.encodeUri,
+            this.decodeUri,
+            this.encodeUriComponent,
+            this.decodeUriComponent,
+        ]);
+    }
+
+    equalTo(other: BrsType): BrsBoolean {
+        if (other.kind === ValueKind.String) {
+            return BrsBoolean.from(other.value === this.intrinsic.value);
+        }
+
+        if (other instanceof RoString) {
+            return BrsBoolean.from(other.intrinsic.value === this.intrinsic.value);
+        }
+
+        return BrsBoolean.False;
+    }
+
+    lessThan(other: BrsType): BrsBoolean {
+        if (other.kind === ValueKind.String) {
+            return this.unbox().lessThan(other);
+        }
+
+        if (other instanceof RoString) {
+            return this.unbox().lessThan(other.unbox());
+        }
+
+        return BrsBoolean.False;
+    }
+
+    greaterThan(other: BrsType): BrsBoolean {
+        if (other.kind === ValueKind.String) {
+            return this.unbox().greaterThan(other);
+        }
+
+        if (other instanceof RoString) {
+            return this.unbox().greaterThan(other.unbox());
+        }
+
+        return BrsBoolean.False;
+    }
+
+    unbox() {
+        return this.intrinsic;
+    }
+
+    toString(_parent?: BrsType): string {
+        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;
+        },
+    });
+
+    /** Appends the first len characters of s to the end of the string. */
+    private appendString = new Callable("AppendString", {
+        signature: {
+            args: [
+                new StdlibArgument("s", ValueKind.String),
+                new StdlibArgument("len", ValueKind.Int32),
+            ],
+            returns: ValueKind.Void,
+        },
+        impl: (_interpreter, s: BrsString, len: Int32) => {
+            this.intrinsic = this.intrinsic.concat(
+                new BrsString(s.value.substr(0, len.getValue()))
+            );
+            return BrsInvalid.Instance;
+        },
+    });
+
+    /** Returns the number of characters in the string. */
+    private len = new Callable("Len", {
+        signature: {
+            args: [],
+            returns: ValueKind.Int32,
+        },
+        impl: _interpreter => {
+            return new Int32(this.intrinsic.value.length);
+        },
+    });
+
+    /** Returns a string consisting of the first len characters of the string. */
+    private left = new Callable("Left", {
+        signature: {
+            args: [new StdlibArgument("len", ValueKind.Int32)],
+            returns: ValueKind.String,
+        },
+        impl: (_interpreter, len: Int32) => {
+            return new BrsString(this.intrinsic.value.substr(0, len.getValue()));
+        },
+    });
+
+    /** Returns a string consisting of the last len characters of the string. */
+    private right = new Callable("Right", {
+        signature: {
+            args: [new StdlibArgument("len", ValueKind.Int32)],
+            returns: ValueKind.String,
+        },
+        impl: (_interpreter, len: Int32) => {
+            let source = this.intrinsic.value;
+            return new BrsString(source.substr(source.length - len.getValue()));
+        },
+    });
+
+    private mid = new Callable(
+        "Mid",
+        /**
+         * Returns a string consisting of the last characters of the string, starting at the
+         * zero-based start_index.
+         */
+        {
+            signature: {
+                args: [new StdlibArgument("start_index", ValueKind.Int32)],
+                returns: ValueKind.String,
+            },
+            impl: (_interpreter, startIndex: Int32) => {
+                return new BrsString(this.intrinsic.value.substr(startIndex.getValue()));
+            },
+        },
+
+        /**
+         * Returns a string consisting of num_chars characters of the string, starting at the
+         * zero-based start_index.
+         */
+        {
+            signature: {
+                args: [
+                    new StdlibArgument("start_index", ValueKind.Int32),
+                    new StdlibArgument("num_chars", ValueKind.Int32),
+                ],
+                returns: ValueKind.String,
+            },
+            impl: (_interpreter, startIndex: Int32, numChars: Int32) => {
+                let source = this.intrinsic.value;
+                return new BrsString(
+                    this.intrinsic.value.substr(startIndex.getValue(), numChars.getValue())
+                );
+            },
+        }
+    );
+
+    private instr = new Callable(
+        "Instr",
+        /** Returns the zero-based index of the first occurrence of substring in the string. */
+        {
+            signature: {
+                args: [new StdlibArgument("substring", ValueKind.String)],
+                returns: ValueKind.Int32,
+            },
+            impl: (_interpreter, substring: BrsString) => {
+                return new Int32(this.intrinsic.value.indexOf(substring.value));
+            },
+        },
+        /**
+         * Returns the zero-based index of the first occurrence of substring in the string, starting
+         * at the specified zero-based start_index.
+         */
+        {
+            signature: {
+                args: [
+                    new StdlibArgument("start_index", ValueKind.Int32),
+                    new StdlibArgument("substring", ValueKind.String),
+                ],
+                returns: ValueKind.Int32,
+            },
+            impl: (_interpreter, startIndex: Int32, substring: BrsString) => {
+                return new Int32(
+                    this.intrinsic.value.indexOf(substring.value, startIndex.getValue())
+                );
+            },
+        }
+    );
+
+    /**
+     * Returns a copy of the string with all instances of fromStr replaced with toStr. If fromStr is
+     * empty the return value is the same as the source string.
+     */
+    private replace = new Callable("Replace", {
+        signature: {
+            args: [
+                new StdlibArgument("from", ValueKind.String),
+                new StdlibArgument("to", ValueKind.String),
+            ],
+            returns: ValueKind.String,
+        },
+        impl: (_interpreter, from: BrsString, to: BrsString) => {
+            if (from.value === "") {
+                return this.intrinsic;
+            }
+
+            return new BrsString(
+                this.intrinsic.value.replace(new RegExp(from.value, "g"), to.value)
+            );
+        },
+    });
+
+    /**
+     * Returns the string with any leading and trailing whitespace characters (space, TAB, LF, CR,
+     * VT, FF, NO-BREAK SPACE, et al) removed.
+     */
+    private trim = new Callable("Trim", {
+        signature: {
+            args: [],
+            returns: ValueKind.String,
+        },
+        impl: _interpreter => {
+            return new BrsString(this.intrinsic.value.trim());
+        },
+    });
+
+    /** Returns the value of the string interpreted as a decimal number. */
+    private toInt = new Callable("ToInt", {
+        signature: {
+            args: [],
+            returns: ValueKind.Int32,
+        },
+        impl: _interpreter => {
+            let int = Math.trunc(Number.parseFloat(this.intrinsic.value));
+
+            if (Number.isNaN(int)) {
+                // non-integers are returned as "0"
+                return new Int32(0);
+            }
+
+            return new Int32(int);
+        },
+    });
+
+    /** Returns the value of the string interpreted as a floating point number. */
+    private toFloat = new Callable("ToFloat", {
+        signature: {
+            args: [],
+            returns: ValueKind.Float,
+        },
+        impl: _interpreter => {
+            let float = Number.parseFloat(this.intrinsic.value);
+
+            if (Number.isNaN(float)) {
+                // non-integers are returned as "0"
+                return new Float(0);
+            }
+
+            return new Float(float);
+        },
+    });
+
+    /**
+     * Splits the string into separate substrings separated by a single delimiter character. Returns
+     * an roList containing each of the substrings. The delimiters are not returned.
+     */
+    private tokenize = new Callable("Tokenize", {
+        signature: {
+            args: [new StdlibArgument("delim", ValueKind.String)],
+            returns: ValueKind.Object,
+        },
+        impl: _interpreter => {
+            _interpreter.stderr.write(
+                "WARNING: tokenize not yet implemented, because it returns an RoList.  Returning `invalid`."
+            );
+            return BrsInvalid.Instance;
+        },
+    });
+
+    /**
+     * Splits the input string using the separator string as a delimiter, and returns an array of
+     * the split token strings (not including the delimiter). An empty separator string indicates
+     * to split the string by character.
+     */
+    private split = new Callable("Split", {
+        signature: {
+            args: [new StdlibArgument("separator", ValueKind.String)],
+            returns: ValueKind.Object,
+        },
+        impl: (_interpreter, separator: BrsString) => {
+            let parts;
+            if (separator.value === "") {
+                // split characters apart, preserving multi-character unicode structures
+                parts = Array.from(this.intrinsic.value);
+            } else {
+                parts = this.intrinsic.value.split(separator.value);
+            }
+
+            return new RoArray(parts.map(part => new BrsString(part)));
+        },
+    });
+
+    /**
+     * Returns the string with certain characters ("'<>&) replaced with the corresponding HTML
+     * entity encoding.
+     */
+    private getEntityEncode = new Callable("GetEntityEncode", {
+        signature: {
+            args: [],
+            returns: ValueKind.String,
+        },
+        impl: _interpreter => {
+            return new BrsString(this.intrinsic.value.replace(/(['"<>&])/g, "\\$1"));
+        },
+    });
+
+    /** URL encodes the specified string per RFC 3986 and returns the encoded string. */
+    private escape = new Callable("Escape", {
+        signature: {
+            args: [],
+            returns: ValueKind.String,
+        },
+        impl: _interpreter => {
+            return new BrsString(
+                // encoding courtesy of
+                // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#Description
+                encodeURIComponent(this.intrinsic.value).replace(
+                    /[!'()*]/g,
+                    c =>
+                        "%" +
+                        c
+                            .charCodeAt(0)
+                            .toString(16)
+                            .toUpperCase()
+                )
+            );
+        },
+    });
+
+    /** URL decodes the specified string per RFC 3986 and returns the decoded string. */
+    private unescape = new Callable("Unescape", {
+        signature: {
+            args: [],
+            returns: ValueKind.String,
+        },
+        impl: _interpreter => {
+            return new BrsString(decodeURIComponent(this.intrinsic.value));
+        },
+    });
+
+    /**
+     * Encode the specified string with escape sequences for reserved Uniform Resource Identifier
+     * (URI) characters.
+     */
+    private encodeUri = new Callable("EncodeUri", {
+        signature: {
+            args: [],
+            returns: ValueKind.String,
+        },
+        impl: _interpreter => {
+            return new BrsString(encodeURI(this.intrinsic.value));
+        },
+    });
+
+    /**
+     * Decode the specified string with escape sequences for reserved Uniform Resource Identifier
+     * (URI) characters.
+     */
+    private decodeUri = new Callable("DecodeUri", {
+        signature: {
+            args: [],
+            returns: ValueKind.String,
+        },
+        impl: _interpreter => {
+            return new BrsString(decodeURI(this.intrinsic.value));
+        },
+    });
+
+    /**
+     * Encode the specified string with escape sequences for reserved Uniform Resource Identifier
+     * (URI) component characters.
+     */
+    private encodeUriComponent = new Callable("EncodeUriComponent", {
+        signature: {
+            args: [],
+            returns: ValueKind.String,
+        },
+        impl: _interpreter => {
+            return new BrsString(encodeURIComponent(this.intrinsic.value));
+        },
+    });
+
+    private decodeUriComponent = new Callable("DecodeUriCOmponent", {
+        signature: {
+            args: [],
+            returns: ValueKind.String,
+        },
+        impl: _interpreter => {
+            return new BrsString(decodeURIComponent(this.intrinsic.value));
+        },
+    });
+}
diff --git a/src/brsTypes/components/Timespan.ts b/src/brsTypes/components/Timespan.ts
index f581a2c..7e67893 100644
--- a/src/brsTypes/components/Timespan.ts
+++ b/src/brsTypes/components/Timespan.ts
@@ -1,4 +1,4 @@
-import { BrsValue, ValueKind, BrsString, BrsInvalid } from "../BrsType";
+import { BrsValue, ValueKind, BrsString, BrsInvalid, BrsBoolean } from "../BrsType";
 import { BrsComponent } from "./BrsComponent";
 import { BrsType } from "..";
 import { Callable, StdlibArgument } from "../Callable";
@@ -30,6 +30,10 @@ export class Timespan extends BrsComponent implements BrsValue {
         return "<Component: roTimespan>";
     }
 
+    equalTo(other: BrsType) {
+        return BrsBoolean.False;
+    }
+
     /** Sets timespan object to the current time */
     private mark = new Callable("mark", {
         signature: {
diff --git a/src/brsTypes/index.ts b/src/brsTypes/index.ts
index a57eec0..303aba6 100644
--- a/src/brsTypes/index.ts
+++ b/src/brsTypes/index.ts
@@ -1,12 +1,21 @@
-import { ValueKind, BrsInvalid, BrsBoolean, BrsString, Uninitialized, Comparable } from "./BrsType";
+import {
+    ValueKind,
+    BrsInvalid,
+    BrsBoolean,
+    BrsString,
+    Uninitialized,
+    Comparable,
+    BrsValue,
+} from "./BrsType";
 import { RoArray } from "./components/RoArray";
 import { RoAssociativeArray } from "./components/RoAssociativeArray";
 import { Int32 } from "./Int32";
 import { Int64 } from "./Int64";
 import { Float } from "./Float";
 import { Double } from "./Double";
-import { Callable, CallableImplementation } from "./Callable";
-import { Lexeme } from "../lexer";
+import { Callable } from "./Callable";
+import { BrsComponent } from "./components/BrsComponent";
+import { RoString } from "./components/RoString";
 
 export * from "./BrsType";
 export * from "./Int32";
@@ -18,6 +27,7 @@ export * from "./components/RoAssociativeArray";
 export * from "./components/Timespan";
 export * from "./components/BrsObjects";
 export * from "./components/RoRegex";
+export * from "./components/RoString";
 export * from "./Callable";
 
 /**
@@ -43,7 +53,7 @@ export function isBrsNumber(value: BrsType): value is BrsNumber {
  * @returns `true` if `value` is a string, otherwise `false`.
  */
 export function isBrsString(value: BrsType): value is BrsString {
-    return value.kind === ValueKind.String;
+    return value.kind === ValueKind.String || value instanceof RoString;
 }
 
 /**
@@ -70,11 +80,7 @@ export function isBrsCallable(value: BrsType): value is Callable {
  * @returns `true` if `value` can be iterated across, otherwise `false`.
  */
 export function isIterable(value: BrsType): value is Iterable {
-    if (value.kind !== ValueKind.Object) {
-        return false;
-    }
-
-    return value.getElements != null;
+    return value instanceof RoArray || value instanceof RoAssociativeArray;
 }
 
 /** The set of BrightScript numeric types. */
@@ -89,5 +95,8 @@ export type BrsPrimitive = BrsInvalid | BrsBoolean | BrsString | BrsNumber;
 /** The set of BrightScript iterable types. */
 export type Iterable = RoArray | RoAssociativeArray;
 
+// this is getting weird - we need a lesThan and greaterThan function?!
+export type AllComponents = { kind: ValueKind.Object } & BrsComponent & BrsValue;
+
 /** The set of all supported types in BrightScript. */
-export type BrsType = BrsPrimitive | Iterable | Callable | Uninitialized;
+export type BrsType = BrsPrimitive | Iterable | Callable | AllComponents | Uninitialized;
diff --git a/src/interpreter/AutoBox.ts b/src/interpreter/AutoBox.ts
new file mode 100644
index 0000000..52dfad7
--- /dev/null
+++ b/src/interpreter/AutoBox.ts
@@ -0,0 +1,10 @@
+import { BrsType, ValueKind, RoString } from "../brsTypes";
+
+export function autobox(maybeIntrinsic: BrsType) {
+    switch (maybeIntrinsic.kind) {
+        case ValueKind.String:
+            return new RoString(maybeIntrinsic);
+        default:
+            return maybeIntrinsic;
+    }
+}
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index a83de04..6bc2ff9 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -18,6 +18,7 @@ import {
     MismatchReason,
     Callable,
     BrsNumber,
+    Comparable,
 } from "../brsTypes";
 
 import { Lexeme } from "../lexer";
@@ -34,6 +35,8 @@ import { Runtime } from "../parser/Statement";
 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 {
@@ -313,15 +316,17 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
          * @returns `true` if `left` and `right` are allowed to be compared to each other with `operator`,
          *          otherwise `false`.
          */
-        function canCompare(left: BrsType, operator: Lexeme, right: BrsType): boolean {
+        function canCheckEquality(left: BrsType, operator: Lexeme, right: BrsType): boolean {
             if (left.kind === ValueKind.Invalid || right.kind === ValueKind.Invalid) {
                 // anything can be checked for *equality* with `invalid`, but greater than / less than comparisons
                 // are type mismatches
                 return operator === Lexeme.Equal || operator === Lexeme.LessGreater;
             }
 
-            // and only primitive non-invalid values can be compared to each other (i.e. no `foo <> []`)
-            return left.kind < ValueKind.Dynamic && right.kind < ValueKind.Dynamic;
+            return (
+                (left.kind < ValueKind.Dynamic || isUnboxable(left)) &&
+                (right.kind < ValueKind.Dynamic || isUnboxable(right))
+            );
         }
 
         switch (lexeme) {
@@ -458,113 +463,131 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
                     );
                 }
             case Lexeme.Greater:
-                if (!canCompare(left, lexeme, right)) {
-                    return this.addError(
-                        new TypeMismatch({
-                            message: "Attempting to compare non-primitive values.",
-                            left: {
-                                type: left,
-                                location: expression.left.location,
-                            },
-                            right: {
-                                type: right,
-                                location: expression.right.location,
-                            },
-                        })
-                    );
+                if (
+                    (isBrsNumber(left) || isBrsString(left)) &&
+                    (isBrsNumber(right) || isBrsString(right))
+                ) {
+                    return left.greaterThan(right);
                 }
 
-                return left.greaterThan(right);
+                return this.addError(
+                    new TypeMismatch({
+                        message: "Attempting to compare non-primitive values.",
+                        left: {
+                            type: left,
+                            location: expression.left.location,
+                        },
+                        right: {
+                            type: right,
+                            location: expression.right.location,
+                        },
+                    })
+                );
+
             case Lexeme.GreaterEqual:
-                if (!canCompare(left, lexeme, right)) {
-                    return this.addError(
-                        new TypeMismatch({
-                            message: "Attempting to compare non-primitive values.",
-                            left: {
-                                type: left,
-                                location: expression.left.location,
-                            },
-                            right: {
-                                type: right,
-                                location: expression.right.location,
-                            },
-                        })
-                    );
+                if (
+                    (isBrsNumber(left) || isBrsString(left)) &&
+                    (isBrsNumber(right) || isBrsString(right))
+                ) {
+                    return left.greaterThan(right).or(left.equalTo(right));
+                } else if (canCheckEquality(left, lexeme, right)) {
+                    return left.equalTo(right);
                 }
 
-                return left.greaterThan(right).or(left.equalTo(right));
+                return this.addError(
+                    new TypeMismatch({
+                        message: "Attempting to compare non-primitive values.",
+                        left: {
+                            type: left,
+                            location: expression.left.location,
+                        },
+                        right: {
+                            type: right,
+                            location: expression.right.location,
+                        },
+                    })
+                );
+
             case Lexeme.Less:
-                if (!canCompare(left, lexeme, right)) {
-                    return this.addError(
-                        new TypeMismatch({
-                            message: "Attempting to compare non-primitive values.",
-                            left: {
-                                type: left,
-                                location: expression.left.location,
-                            },
-                            right: {
-                                type: right,
-                                location: expression.right.location,
-                            },
-                        })
-                    );
+                if (
+                    (isBrsNumber(left) || isBrsString(left)) &&
+                    (isBrsNumber(right) || isBrsString(right))
+                ) {
+                    return left.lessThan(right);
                 }
 
-                return left.lessThan(right);
+                return this.addError(
+                    new TypeMismatch({
+                        message: "Attempting to compare non-primitive values.",
+                        left: {
+                            type: left,
+                            location: expression.left.location,
+                        },
+                        right: {
+                            type: right,
+                            location: expression.right.location,
+                        },
+                    })
+                );
             case Lexeme.LessEqual:
-                if (!canCompare(left, lexeme, right)) {
-                    return this.addError(
-                        new TypeMismatch({
-                            message: "Attempting to compare non-primitive values.",
-                            left: {
-                                type: left,
-                                location: expression.left.location,
-                            },
-                            right: {
-                                type: right,
-                                location: expression.right.location,
-                            },
-                        })
-                    );
+                if (
+                    (isBrsNumber(left) || isBrsString(left)) &&
+                    (isBrsNumber(right) || isBrsString(right))
+                ) {
+                    return left.lessThan(right).or(left.equalTo(right));
+                } else if (canCheckEquality(left, lexeme, right)) {
+                    return left.equalTo(right);
                 }
 
-                return left.lessThan(right).or(left.equalTo(right));
+                return this.addError(
+                    new TypeMismatch({
+                        message: "Attempting to compare non-primitive values.",
+                        left: {
+                            type: left,
+                            location: expression.left.location,
+                        },
+                        right: {
+                            type: right,
+                            location: expression.right.location,
+                        },
+                    })
+                );
             case Lexeme.Equal:
-                if (!canCompare(left, lexeme, right)) {
-                    return this.addError(
-                        new TypeMismatch({
-                            message: "Attempting to compare non-primitive values.",
-                            left: {
-                                type: left,
-                                location: expression.left.location,
-                            },
-                            right: {
-                                type: right,
-                                location: expression.right.location,
-                            },
-                        })
-                    );
+                if (canCheckEquality(left, lexeme, right)) {
+                    return left.equalTo(right);
                 }
 
-                return left.equalTo(right);
+                return this.addError(
+                    new TypeMismatch({
+                        message: "Attempting to compare non-primitive values.",
+                        left: {
+                            type: left,
+                            location: expression.left.location,
+                        },
+                        right: {
+                            type: right,
+                            location: expression.right.location,
+                        },
+                    })
+                );
             case Lexeme.LessGreater:
-                if (!canCompare(left, lexeme, right)) {
-                    return this.addError(
-                        new TypeMismatch({
-                            message: "Attempting to compare non-primitive values.",
-                            left: {
-                                type: left,
-                                location: expression.left.location,
-                            },
-                            right: {
-                                type: right,
-                                location: expression.right.location,
-                            },
-                        })
-                    );
+                if (canCheckEquality(left, lexeme, right)) {
+                    return left.equalTo(right).not();
                 }
 
-                return left.equalTo(right).not();
+                return this.addError(
+                    new TypeMismatch({
+                        message: "Attempting to compare non-primitive values.",
+                        left: {
+                            type: left,
+                            location: expression.left.location,
+                        },
+                        right: {
+                            type: right,
+                            location: expression.right.location,
+                        },
+                    })
+                );
             case Lexeme.And:
                 if (isBrsBoolean(left) && !left.toBoolean()) {
                     // short-circuit ANDs - don't evaluate RHS if LHS is false
@@ -709,10 +732,12 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
 
     visitCall(expression: Expr.Call) {
         let functionName = "[anonymous function]";
-        if (expression.callee instanceof Expr.Variable) {
-            if (expression.callee.name.text) {
-                functionName = expression.callee.name.text;
-            }
+        // TODO: autobox
+        if (
+            expression.callee instanceof Expr.Variable ||
+            expression.callee instanceof Expr.DottedGet
+        ) {
+            functionName = expression.callee.name.text;
         }
 
         // evaluate the function to call (it could be the result of another function call)
@@ -742,6 +767,8 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
                     expression.callee instanceof Expr.IndexedGet
                 ) {
                     let maybeM = this.evaluate(expression.callee.obj);
+                    maybeM = isBoxable(maybeM) ? maybeM.box() : maybeM;
+
                     if (maybeM.kind === ValueKind.Object) {
                         if (maybeM instanceof RoAssociativeArray) {
                             mPointer = maybeM;
@@ -880,20 +907,25 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
             } catch (err) {
                 return this.addError(new BrsError(err.message, expression.name.location));
             }
-        } else if (source instanceof BrsComponent) {
+        }
+
+        let boxedSource = isBoxable(source) ? source.box() : source;
+        if (boxedSource instanceof BrsComponent) {
             try {
-                return source.getMethod(expression.name.text) || BrsInvalid.Instance;
+                return boxedSource.getMethod(expression.name.text) || BrsInvalid.Instance;
             } catch (err) {
                 return this.addError(new BrsError(err.message, expression.name.location));
             }
         } else {
-            throw new TypeMismatch({
-                message: "Attempting to retrieve property from non-iterable value",
-                left: {
-                    type: source,
-                    location: expression.location,
-                },
-            });
+            return this.addError(
+                new TypeMismatch({
+                    message: "Attempting to retrieve property from non-iterable value",
+                    left: {
+                        type: source,
+                        location: expression.location,
+                    },
+                })
+            );
         }
     }
 
diff --git a/src/preprocessor/Manifest.ts b/src/preprocessor/Manifest.ts
index 609d9a8..13fe573 100644
--- a/src/preprocessor/Manifest.ts
+++ b/src/preprocessor/Manifest.ts
@@ -27,7 +27,17 @@ export async function getManifest(rootDir: string): Promise<Manifest> {
     } catch (err) {
         return new Map();
     }
+    return parseManifest(contents);
+}
 
+/**
+ * Attempts to parse a `manifest` file's contents into a map of string to JavaScript
+ * number, string, or boolean.
+ * @param contents the text contents of a manifest file.
+ * @returns a Promise that resolves to a map of string to JavaScript number, string, or boolean,
+ *          representing the manifest file's contents
+ */
+export function parseManifest(contents: string) {
     let keyValuePairs = contents
         // for each line
         .split("\n")


Test patch:
diff --git a/test/brsTypes/String.test.js b/test/brsTypes/String.test.js
index 4668d6e..da60b80 100644
--- a/test/brsTypes/String.test.js
+++ b/test/brsTypes/String.test.js
@@ -1,33 +1,70 @@
-const BrsTypes = require("../../lib/brsTypes");
+const brs = require("brs");
+const { BrsBoolean, BrsString, RoString } = brs.types;
 
 describe("String", () => {
     describe("lexical comparisons", () => {
-        let a = new BrsTypes.BrsString("alpha");
-        let b = new BrsTypes.BrsString("bravo");
-        let capitalB = new BrsTypes.BrsString("BRAVO");
+        describe("with primitive", () => {
+            let a = new BrsString("alpha");
+            let b = new BrsString("bravo");
+            let capitalB = new BrsString("BRAVO");
 
-        test("less than", () => {
-            expect(a.lessThan(b)).toBe(BrsTypes.BrsBoolean.True);
-            expect(a.lessThan(capitalB)).toBe(BrsTypes.BrsBoolean.False);
-        });
+            test("less than", () => {
+                expect(a.lessThan(b)).toBe(BrsBoolean.True);
+                expect(b.lessThan(a)).toBe(BrsBoolean.False);
+                expect(a.lessThan(capitalB)).toBe(BrsBoolean.False);
+                expect(capitalB.lessThan(a)).toBe(BrsBoolean.True);
+            });
+
+            test("greater than", () => {
+                expect(a.greaterThan(b)).toBe(BrsBoolean.False);
+                expect(b.greaterThan(a)).toBe(BrsBoolean.True);
+                expect(a.lessThan(capitalB)).toBe(BrsBoolean.False);
+                expect(capitalB.lessThan(a)).toBe(BrsBoolean.True);
+            });
 
-        test("greater than", () => {
-            expect(a.greaterThan(b)).toBe(BrsTypes.BrsBoolean.False);
-            expect(a.lessThan(capitalB)).toBe(BrsTypes.BrsBoolean.False);
+            test("equal to", () => {
+                let differentA = new BrsString("alpha");
+                expect(a.equalTo(differentA)).toBe(BrsBoolean.True);
+                expect(differentA.equalTo(a)).toBe(BrsBoolean.True);
+                expect(b.equalTo(capitalB)).toBe(BrsBoolean.False);
+                expect(capitalB.equalTo(b)).toBe(BrsBoolean.False);
+            });
         });
 
-        test("equal to", () => {
-            let differentA = new BrsTypes.BrsString("alpha");
-            expect(a.equalTo(differentA)).toBe(BrsTypes.BrsBoolean.True);
-            expect(b.equalTo(capitalB)).toBe(BrsTypes.BrsBoolean.False);
+        describe("with RoString", () => {
+            let a = new BrsString("alpha");
+            let b = new RoString(new BrsString("bravo"));
+            let capitalB = new BrsString("BRAVO");
+
+            test("less than", () => {
+                expect(a.lessThan(b)).toBe(BrsBoolean.True);
+                expect(b.lessThan(a)).toBe(BrsBoolean.False);
+                expect(a.lessThan(capitalB)).toBe(BrsBoolean.False);
+                expect(capitalB.lessThan(a)).toBe(BrsBoolean.True);
+            });
+
+            test("greater than", () => {
+                expect(a.greaterThan(b)).toBe(BrsBoolean.False);
+                expect(b.greaterThan(a)).toBe(BrsBoolean.True);
+                expect(a.lessThan(capitalB)).toBe(BrsBoolean.False);
+                expect(capitalB.lessThan(a)).toBe(BrsBoolean.True);
+            });
+
+            test("equal to", () => {
+                let boxedA = new RoString(new BrsString("alpha"));
+                expect(a.equalTo(boxedA)).toBe(BrsBoolean.True);
+                expect(boxedA.equalTo(a)).toBe(BrsBoolean.True);
+                expect(b.equalTo(capitalB)).toBe(BrsBoolean.False);
+                expect(capitalB.equalTo(b)).toBe(BrsBoolean.False);
+            });
         });
     });
 
     it("concatenates with other strings", () => {
-        let deliveryFor = new BrsTypes.BrsString("Pizza delivery for: ");
-        let recipient = new BrsTypes.BrsString("I. C. Weiner");
+        let deliveryFor = new BrsString("Pizza delivery for: ");
+        let recipient = new BrsString("I. C. Weiner");
         expect(deliveryFor.concat(recipient)).toEqual(
-            new BrsTypes.BrsString("Pizza delivery for: I. C. Weiner")
+            new BrsString("Pizza delivery for: I. C. Weiner")
         );
     });
 });
diff --git a/test/brsTypes/components/RoArray.test.js b/test/brsTypes/components/RoArray.test.js
index 4f0b9dd..aabe110 100644
--- a/test/brsTypes/components/RoArray.test.js
+++ b/test/brsTypes/components/RoArray.test.js
@@ -3,16 +3,8 @@ const { RoArray, BrsBoolean, BrsString, Int32, BrsInvalid } = brs.types;
 const BrsError = require("../../../lib/Error");
 const { Interpreter } = require("../../../lib/interpreter");
 
-describe("Array", () => {
+describe("RoArray", () => {
     describe("comparisons", () => {
-        it("is less than nothing", () => {
-            let a = new RoArray([]);
-            expect(a.lessThan(a)).toBe(BrsBoolean.False);
-        });
-        it("is greater than nothing", () => {
-            let a = new RoArray([]);
-            expect(a.greaterThan(a)).toBe(BrsBoolean.False);
-        });
         it("is equal to nothing", () => {
             let a = new RoArray([]);
             expect(a.equalTo(a)).toBe(BrsBoolean.False);
diff --git a/test/brsTypes/components/RoAssociativeArray.test.js b/test/brsTypes/components/RoAssociativeArray.test.js
index e3c8d1f..b7f1f99 100644
--- a/test/brsTypes/components/RoAssociativeArray.test.js
+++ b/test/brsTypes/components/RoAssociativeArray.test.js
@@ -10,16 +10,8 @@ const {
 } = brs.types;
 const { Interpreter } = require("../../../lib/interpreter");
 
-describe("AssociativeArray", () => {
+describe("RoAssociativeArray", () => {
     describe("comparisons", () => {
-        it("is less than nothing", () => {
-            let a = new RoAssociativeArray([]);
-            expect(a.lessThan(a)).toBe(BrsBoolean.False);
-        });
-        it("is greater than nothing", () => {
-            let a = new RoAssociativeArray([]);
-            expect(a.greaterThan(a)).toBe(BrsBoolean.False);
-        });
         it("is equal to nothing", () => {
             let a = new RoAssociativeArray([]);
             expect(a.equalTo(a)).toBe(BrsBoolean.False);
diff --git a/test/brsTypes/components/RoRegex.js b/test/brsTypes/components/RoRegex.test.js
similarity index 91%
rename from test/brsTypes/components/RoRegex.js
rename to test/brsTypes/components/RoRegex.test.js
index e39dbe7..f1b08c3 100644
--- a/test/brsTypes/components/RoRegex.js
+++ b/test/brsTypes/components/RoRegex.test.js
@@ -11,7 +11,7 @@ describe("RoRegex", () => {
 
     describe("isMatch", () => {
         it("matches strings with case sensitive flag", () => {
-            let regx = newRoRegex(new BrsString("hello_[0-9]*_world"));
+            let regx = new RoRegex(new BrsString("hello_[0-9]*_world"));
             let isMatch = regx.getMethod("isMatch");
             expect(isMatch).toBeTruthy();
 
@@ -25,7 +25,7 @@ describe("RoRegex", () => {
         });
 
         it("matches strings with case insensitive flag", () => {
-            let regx = newRoRegex(new BrsString("hello_[0-9]*_world"), new BrsString("i"));
+            let regx = new RoRegex(new BrsString("hello_[0-9]*_world"), new BrsString("i"));
             let isMatch = regx.getMethod("isMatch");
             expect(isMatch).toBeTruthy();
 
@@ -39,7 +39,7 @@ describe("RoRegex", () => {
         });
 
         it("matches a string in ISO8601 format", () => {
-            let regx = newRoRegex(
+            let regx = new RoRegex(
                 new BrsString("P(\\d+Y)?(\\d+M)?(\\d+D)?T(\\d+H)?(\\d+M)?(\\d+(.\\d+)?S)?")
             );
             let isMatch = regx.getMethod("isMatch");
@@ -57,7 +57,7 @@ describe("RoRegex", () => {
 
     describe("match", () => {
         it("doesn't match string", () => {
-            let rgx = newRoRegex(new BrsString("(a|(z))(bc)"));
+            let rgx = new RoRegex(new BrsString("(a|(z))(bc)"));
             let match = rgx.getMethod("match");
             expect(match).toBeTruthy();
 
@@ -69,7 +69,7 @@ describe("RoRegex", () => {
         });
 
         it("matches groups in string", () => {
-            let rgx = newRoRegex(new BrsString("(a|(z))(bc)"));
+            let rgx = new RoRegex(new BrsString("(a|(z))(bc)"));
             let match = rgx.getMethod("match");
             expect(match).toBeTruthy();
 
@@ -80,12 +80,12 @@ describe("RoRegex", () => {
             expect(count.value).toBe(4);
             expect(result.get(new Int32(0)).value).toBe("abc"); //Entire match
             expect(result.get(new Int32(1)).value).toBe("a");
-            expect(result.get(new Int32(2))).toEqual(BrsInvalid.Instance);
+            expect(result.get(new Int32(2)).value).toBe("");
             expect(result.get(new Int32(3)).value).toBe("bc");
         });
 
         it("matches string in hh:mm:ss.ms date format", () => {
-            let regx = newRoRegex(
+            let regx = new RoRegex(
                 new BrsString("(([0-9]{2}):)?(([0-9]{2}):)?([0-9]{2})\\.([0-9]{3})")
             );
             let match = regx.getMethod("match");
@@ -99,7 +99,7 @@ describe("RoRegex", () => {
         });
 
         it("matches strings using beginning and end of input", () => {
-            let regx = newRoRegex(
+            let regx = new RoRegex(
                 new BrsString("^(.*:)\\/\\/([A-Za-z0-9\\-\\.]+)?(:[0-9]+)?\\/?(.*)$")
             );
             let match = regx.getMethod("match");
@@ -127,7 +127,7 @@ describe("RoRegex", () => {
 
     describe("replace", () => {
         it("replaces first matched instance from string", () => {
-            let rgx = newRoRegex(new BrsString("-"));
+            let rgx = new RoRegex(new BrsString("-"));
             let replace = rgx.getMethod("replace");
             expect(replace).toBeTruthy();
 
@@ -137,7 +137,7 @@ describe("RoRegex", () => {
         });
 
         it("replaces string by using positional replacement patterns", () => {
-            let rgx = newRoRegex(new BrsString("S([0-9]+) E([0-9]+)"), new BrsString("i"));
+            let rgx = new RoRegex(new BrsString("S([0-9]+) E([0-9]+)"), new BrsString("i"));
             let replace = rgx.getMethod("replace");
             expect(replace).toBeTruthy();
 
@@ -153,7 +153,7 @@ describe("RoRegex", () => {
 
     describe("replaceAll", () => {
         it("replaces all matched instances from string", () => {
-            let rgx = newRoRegex(new BrsString("-"));
+            let rgx = new RoRegex(new BrsString("-"));
             let replaceAll = rgx.getMethod("replaceall");
             expect(replaceAll).toBeTruthy();
 
@@ -169,7 +169,7 @@ describe("RoRegex", () => {
 
     describe("split", () => {
         it("splits in the correct number of items", () => {
-            let rgx = newRoRegex(new BrsString(","));
+            let rgx = new RoRegex(new BrsString(","));
             let split = rgx.getMethod("split");
             expect(split).toBeTruthy();
 
@@ -182,7 +182,7 @@ describe("RoRegex", () => {
 
     describe("matchAll", () => {
         it("matches all patterns in the string", () => {
-            let rgx = newRoRegex(new BrsString("\\d+"));
+            let rgx = new RoRegex(new BrsString("\\d+"));
             let matchAll = rgx.getMethod("matchall");
             expect(matchAll).toBeTruthy();
 
diff --git a/test/brsTypes/components/RoString.test.js b/test/brsTypes/components/RoString.test.js
new file mode 100644
index 0000000..7eefb4e
--- /dev/null
+++ b/test/brsTypes/components/RoString.test.js
@@ -0,0 +1,584 @@
+const brs = require("brs");
+const { Int32, Float, BrsString, RoString, RoArray, BrsBoolean, Callable } = brs.types;
+const { Interpreter } = require("../../../lib/interpreter");
+
+describe("RoString", () => {
+    describe("equality", () => {
+        it("compares to intrinsic strings", () => {
+            let a = new RoString(new BrsString("foo"));
+            let b = new BrsString("foo");
+            let c = new BrsString("bar");
+
+            expect(a.equalTo(b)).toBe(BrsBoolean.True);
+            expect(a.equalTo(c)).toBe(BrsBoolean.False);
+        });
+
+        it("compares to boxed strings", () => {
+            let a = new RoString(new BrsString("foo"));
+            let b = new RoString(new BrsString("foo"));
+            let c = new RoString(new BrsString("bar"));
+
+            expect(a.equalTo(b)).toBe(BrsBoolean.True);
+            expect(a.equalTo(c)).toBe(BrsBoolean.False);
+        });
+    });
+
+    test("toString", () => {
+        expect(new RoString(new BrsString("A1b2C#☃︎")).toString()).toBe("A1b2C#☃︎");
+    });
+
+    describe("ifStringOps", () => {
+        let interpreter;
+
+        beforeEach(() => {
+            interpreter = new Interpreter();
+        });
+
+        describe("setString", () => {
+            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("appendString", () => {
+            let s, appendString;
+
+            beforeEach(() => {
+                s = new RoString(new BrsString("before"));
+                appendString = s.getMethod("appendString");
+                expect(appendString).toBeInstanceOf(Callable);
+            });
+
+            it("appends positive `len` characters", () => {
+                appendString.call(interpreter, new BrsString("after"), new Int32(3));
+                expect(s.intrinsic).toEqual(new BrsString("beforeaft"));
+            });
+
+            it("appends nothing for zero `len`", () => {
+                appendString.call(interpreter, new BrsString("after"), new Int32(0));
+                expect(s.intrinsic).toEqual(new BrsString("before"));
+            });
+
+            it("appends nothing for negative `len`", () => {
+                appendString.call(interpreter, new BrsString("after"), new Int32(-1));
+                expect(s.intrinsic).toEqual(new BrsString("before"));
+            });
+        });
+
+        describe("len", () => {
+            it("returns the length of the intrinsic string", () => {
+                let s = new RoString(new BrsString("☃☃☃"));
+                let len = s.getMethod("len");
+                expect(len).toBeInstanceOf(Callable);
+                expect(len.call(interpreter)).toEqual(new Int32(3));
+            });
+        });
+
+        describe("left", () => {
+            let left;
+
+            beforeEach(() => {
+                let s = new RoString(new BrsString("☃ab"));
+                left = s.getMethod("left");
+                expect(left).toBeInstanceOf(Callable);
+            });
+
+            it("returns the entire string for `len` greater than string length", () => {
+                expect(left.call(interpreter, new Int32(500))).toEqual(new BrsString("☃ab"));
+            });
+
+            it("returns the first `len` characters for positive `len`", () => {
+                expect(left.call(interpreter, new Int32(2))).toEqual(new BrsString("☃a"));
+            });
+
+            it("returns an empty string for non-positive `len`", () => {
+                expect(left.call(interpreter, new Int32(0))).toEqual(new BrsString(""));
+                expect(left.call(interpreter, new Int32(-25))).toEqual(new BrsString(""));
+            });
+        });
+
+        describe("right", () => {
+            let right;
+
+            beforeEach(() => {
+                let s = new RoString(new BrsString("☃ab"));
+                right = s.getMethod("right");
+                expect(right).toBeInstanceOf(Callable);
+            });
+
+            it("returns the entire string for `len` greater than string length", () => {
+                expect(right.call(interpreter, new Int32(500))).toEqual(new BrsString("☃ab"));
+            });
+
+            it("returns the last `len` characters for positive `len`", () => {
+                expect(right.call(interpreter, new Int32(2))).toEqual(new BrsString("ab"));
+            });
+
+            it("returns an empty string for non-positive `len`", () => {
+                expect(right.call(interpreter, new Int32(0))).toEqual(new BrsString(""));
+                expect(right.call(interpreter, new Int32(-25))).toEqual(new BrsString(""));
+            });
+        });
+
+        describe("mid", () => {
+            let mid;
+
+            beforeEach(() => {
+                //                                  0   0   0   1   1   2   2
+                //                                  0   4   8   2   6   0   4
+                let s = new RoString(new BrsString("Lorem ipsum dolor sit aMeT"));
+                mid = s.getMethod("mid");
+                expect(mid).toBeInstanceOf(Callable);
+            });
+
+            describe("without `len`", () => {
+                it("returns all characters after positive `start_point`", () => {
+                    expect(mid.call(interpreter, new Int32(12))).toEqual(
+                        new BrsString("dolor sit aMeT")
+                    );
+                });
+
+                it("returns entire string for zero `start_point`", () => {
+                    expect(mid.call(interpreter, new Int32(0))).toEqual(
+                        new BrsString("Lorem ipsum dolor sit aMeT")
+                    );
+                });
+
+                it("returns entire string for negative `start_point`", () => {
+                    expect(mid.call(interpreter, new Int32(-30))).toEqual(
+                        new BrsString("Lorem ipsum dolor sit aMeT")
+                    );
+                });
+
+                it("returns empty string for `start_point` greater than string length", () => {
+                    expect(mid.call(interpreter, new Int32(26))).toEqual(new BrsString(""));
+                });
+            });
+
+            describe("with `len`", () => {
+                it("returns `len` characters after positive `start_point`", () => {
+                    expect(mid.call(interpreter, new Int32(12), new Int32(7))).toEqual(
+                        new BrsString("dolor s")
+                    );
+                });
+
+                it("returns string starting at 0 for negative `start_point` and positive `len`", () => {
+                    expect(mid.call(interpreter, new Int32(-30), new Int32(7))).toEqual(
+                        new BrsString("Lorem i")
+                    );
+                });
+
+                it("returns empty string for negative `start_point` and negative `len`", () => {
+                    expect(mid.call(interpreter, new Int32(-30), new Int32(-9))).toEqual(
+                        new BrsString("")
+                    );
+                });
+
+                it("returns empty string for `start_point` greater than string length", () => {
+                    expect(mid.call(interpreter, new Int32(26), new Int32(5))).toEqual(
+                        new BrsString("")
+                    );
+                });
+            });
+        });
+
+        describe("instr", () => {
+            let instr;
+
+            beforeEach(() => {
+                //                                  0   0   0   1   1   2   2
+                //                                  0   4   8   2   6   0   4
+                let s = new RoString(new BrsString("Monday, Tuesday, Happy Days"));
+                instr = s.getMethod("instr");
+                expect(instr).toBeInstanceOf(Callable);
+            });
+
+            describe("without start_index", () => {
+                it("returns the index of the first occurrence", () => {
+                    expect(instr.call(interpreter, new BrsString("day"))).toEqual(new Int32(3));
+                });
+
+                it("returns -1 for not-found substrings", () => {
+                    expect(instr.call(interpreter, new BrsString("Fonzie"))).toEqual(new Int32(-1));
+                });
+
+                it.todo("returns 0 for empty substrings");
+                // TODO: compare to RBI
+                // () => expect(instr.call(interpreter, new BrsString(""))).toEqual(new Int32(-1));
+            });
+
+            describe("with start_index", () => {
+                it("returns the index of the first occurrence after `start_index`", () => {
+                    expect(instr.call(interpreter, new Int32(5), new BrsString("day"))).toEqual(
+                        new Int32(12)
+                    );
+                });
+
+                it("returns -1 for not-found substrings", () => {
+                    expect(instr.call(interpreter, new Int32(5), new BrsString("Monday"))).toEqual(
+                        new Int32(-1)
+                    );
+                });
+
+                it.todo("returns start_index for empty substrings");
+                // TODO: compare to RBI
+                // () => expect(instr.call(interpreter, new BrsString(""))).toEqual(new Int32(-1));
+            });
+        });
+
+        describe("replace", () => {
+            let replace;
+
+            beforeEach(() => {
+                let s = new RoString(new BrsString("tossed salad and scrambled eggs"));
+                replace = s.getMethod("replace");
+                expect(replace).toBeInstanceOf(Callable);
+            });
+
+            it("replaces all instances of `from` with `to`", () => {
+                expect(replace.call(interpreter, new BrsString("s"), new BrsString("$"))).toEqual(
+                    new BrsString("to$$ed $alad and $crambled egg$")
+                );
+            });
+
+            it("returns the original string for empty `from`", () => {
+                expect(
+                    replace.call(
+                        interpreter,
+                        new BrsString(""),
+                        new BrsString("oh baby I hear the blues a-callin'")
+                    )
+                ).toEqual(new BrsString("tossed salad and scrambled eggs"));
+            });
+        });
+
+        describe("trim", () => {
+            let trim;
+
+            beforeEach(() => {
+                let whitespace = [
+                    String.fromCharCode(0x0a), // newline
+                    String.fromCharCode(0x0b), // vertical tab
+                    String.fromCharCode(0x0c), // form feed
+                    String.fromCharCode(0x0d), // carriage return
+                    String.fromCharCode(0xa0), // non-breaking space
+                    "\t", // just a regular tab
+                    " ", // just a regular space
+                ].join("");
+
+                let s = new RoString(new BrsString(whitespace + "hello" + whitespace));
+                trim = s.getMethod("trim");
+                expect(trim).toBeInstanceOf(Callable);
+            });
+
+            it("removes leading and trailing whitespace", () => {
+                expect(trim.call(interpreter)).toEqual(new BrsString("hello"));
+            });
+        });
+
+        describe("toInt", () => {
+            it("returns 0 for non-numbers", () => {
+                let s = new RoString(new BrsString("I'm just a bill."));
+                let toInt = s.getMethod("toInt");
+                expect(toInt).toBeInstanceOf(Callable);
+
+                expect(toInt.call(interpreter)).toEqual(new Int32(0));
+            });
+
+            it("returns 0 for hex strings", () => {
+                let s = new RoString(new BrsString("&hFF"));
+                let toInt = s.getMethod("toInt");
+                expect(toInt).toBeInstanceOf(Callable);
+
+                expect(toInt.call(interpreter)).toEqual(new Int32(0));
+            });
+
+            it("converts string-formatted integers to Integer type", () => {
+                let s = new RoString(new BrsString("112358"));
+                let toInt = s.getMethod("toInt");
+                expect(toInt).toBeInstanceOf(Callable);
+
+                expect(toInt.call(interpreter)).toEqual(new Int32(112358));
+            });
+
+            it("truncates string-formatted floats to Integer type", () => {
+                let negative = new RoString(new BrsString("-1.9"));
+                let negativeToInt = negative.getMethod("toInt");
+                expect(negativeToInt).toBeInstanceOf(Callable);
+                expect(negativeToInt.call(interpreter)).toEqual(new Int32(-1));
+
+                let positive = new RoString(new BrsString("1.9"));
+                let positiveToInt = positive.getMethod("toInt");
+                expect(positiveToInt).toBeInstanceOf(Callable);
+                expect(positiveToInt.call(interpreter)).toEqual(new Int32(1));
+            });
+        });
+
+        describe("toFloat", () => {
+            it("returns 0 for non-numbers", () => {
+                let s = new RoString(new BrsString("I'm just a bill."));
+                let toFloat = s.getMethod("toFloat");
+                expect(toFloat).toBeInstanceOf(Callable);
+
+                expect(toFloat.call(interpreter)).toEqual(new Float(0));
+            });
+
+            it("returns 0 for hex strings", () => {
+                let s = new RoString(new BrsString("&hFF"));
+                let toFloat = s.getMethod("toFloat");
+                expect(toFloat).toBeInstanceOf(Callable);
+
+                expect(toFloat.call(interpreter)).toEqual(new Float(0));
+            });
+
+            it("converts string-formatted integers to Float type", () => {
+                let s = new RoString(new BrsString("112358"));
+                let toFloat = s.getMethod("toFloat");
+                expect(toFloat).toBeInstanceOf(Callable);
+
+                expect(toFloat.call(interpreter)).toEqual(new Float(112358));
+            });
+
+            it("converts string-formatted floats to Float type", () => {
+                let negative = new RoString(new BrsString("-1.9"));
+                let negativeToFloat = negative.getMethod("toFloat");
+                expect(negativeToFloat).toBeInstanceOf(Callable);
+                expect(negativeToFloat.call(interpreter)).toEqual(new Float(-1.9));
+
+                let positive = new RoString(new BrsString("1.9"));
+                let positiveToFloat = positive.getMethod("toFloat");
+                expect(positiveToFloat).toBeInstanceOf(Callable);
+                expect(positiveToFloat.call(interpreter)).toEqual(new Float(1.9));
+            });
+        });
+
+        describe("tokenize", () => {
+            let tokenize;
+
+            beforeEach(() => {
+                let s = new RoString(new BrsString("good dog"));
+                tokenize = s.getMethod("tokenize");
+                expect(tokenize).toBeInstanceOf(Callable);
+            });
+
+            // TODO: implement after RoList exists
+            it.todo("splits by single-character delimiter");
+            it.todo("splits by multi-character delimiter");
+        });
+
+        describe("split", () => {
+            let split;
+
+            beforeEach(() => {
+                let s = new RoString(new BrsString("🐶good dog🐶"));
+                split = s.getMethod("split");
+                expect(split).toBeInstanceOf(Callable);
+            });
+
+            it("splits characters with an empty string", () => {
+                let result = split.call(interpreter, new BrsString(""));
+                expect(result).toBeInstanceOf(RoArray);
+                expect(result.elements).toEqual(
+                    ["🐶", "g", "o", "o", "d", " ", "d", "o", "g", "🐶"].map(c => new BrsString(c))
+                );
+            });
+
+            it("returns one section for not-found delimiters", () => {
+                let result = split.call(interpreter, new BrsString("/"));
+                expect(result).toBeInstanceOf(RoArray);
+                expect(result.elements).toEqual([new BrsString("🐶good dog🐶")]);
+            });
+
+            it("returns empty strings for leading and trailing matches", () => {
+                let result = split.call(interpreter, new BrsString("🐶"));
+                expect(result).toBeInstanceOf(RoArray);
+                expect(result.elements).toEqual([
+                    new BrsString(""),
+                    new BrsString("good dog"),
+                    new BrsString(""),
+                ]);
+            });
+
+            it("splits on multi-character sequences", () => {
+                let result = split.call(interpreter, new BrsString("oo"));
+                expect(result).toBeInstanceOf(RoArray);
+                expect(result.elements).toEqual([new BrsString("🐶g"), new BrsString("d dog🐶")]);
+            });
+        });
+
+        describe("getEntityEncode", () => {
+            it("escapes five special characters", () => {
+                let s = new RoString(
+                    new BrsString(`Let's watch <a href="example.com">Cagney & Lacey</a>!`)
+                );
+                let getEntityEncode = s.getMethod("getEntityEncode");
+                expect(getEntityEncode).toBeInstanceOf(Callable);
+
+                expect(getEntityEncode.call(interpreter)).toEqual(
+                    new BrsString(
+                        String.raw`Let\'s watch \<a href=\"example.com\"\>Cagney \& Lacey\</a\>!`
+                    )
+                );
+            });
+        });
+
+        describe("escape", () => {
+            it("escapes characters in the ascii range", () => {
+                let s = new RoString(new BrsString("@&=+/#!*ABcde_-"));
+                let escape = s.getMethod("escape");
+                expect(escape).toBeInstanceOf(Callable);
+
+                expect(escape.call(interpreter)).toEqual(
+                    new BrsString("%40%26%3D%2B%2F%23%21%2AABcde_-")
+                );
+            });
+
+            it("breaks unicode characters into UTF-8 escape sequences", () => {
+                let s = new RoString(new BrsString("•"));
+                let escape = s.getMethod("escape");
+                expect(escape).toBeInstanceOf(Callable);
+
+                expect(escape.call(interpreter)).toEqual(new BrsString("%E2%80%A2"));
+            });
+        });
+
+        describe("unescape", () => {
+            it("unescapes characters in the ascii range", () => {
+                let s = new RoString(new BrsString("%40%26%3D%2B%2F%23%21%2AABcde_-"));
+                let unescape = s.getMethod("unescape");
+                expect(unescape).toBeInstanceOf(Callable);
+
+                expect(unescape.call(interpreter)).toEqual(new BrsString("@&=+/#!*ABcde_-"));
+            });
+
+            it("combines UTF-8 escape sequences into non-ASCII characters", () => {
+                let s = new RoString(new BrsString("%E2%80%A2"));
+                let unescape = s.getMethod("unescape");
+                expect(unescape).toBeInstanceOf(Callable);
+
+                expect(unescape.call(interpreter)).toEqual(new BrsString("•"));
+            });
+        });
+
+        describe("encodeUri", () => {
+            it("URI-encodes ASCII strings", () => {
+                let s = new RoString(
+                    new BrsString("http://example.com/my test.asp?first=jane&last=doe")
+                );
+                let encodeUri = s.getMethod("encodeUri");
+                expect(encodeUri).toBeInstanceOf(Callable);
+
+                expect(encodeUri.call(interpreter)).toEqual(
+                    new BrsString("http://example.com/my%20test.asp?first=jane&last=doe")
+                );
+            });
+
+            it("encodes non-ascii strings as UTF-8 escape sequences", () => {
+                let s = new RoString(new BrsString("http://example.com/?bullet=•"));
+                let encodeUri = s.getMethod("encodeUri");
+                expect(encodeUri).toBeInstanceOf(Callable);
+
+                expect(encodeUri.call(interpreter)).toEqual(
+                    new BrsString("http://example.com/?bullet=%E2%80%A2")
+                );
+            });
+        });
+
+        describe("decodeUri", () => {
+            it("URI-decodes ASCII strings", () => {
+                let s = new RoString(
+                    new BrsString("http://example.com/my%20test.asp?first=jane&last=doe")
+                );
+                let decodeUri = s.getMethod("decodeUri");
+                expect(decodeUri).toBeInstanceOf(Callable);
+
+                expect(decodeUri.call(interpreter)).toEqual(
+                    new BrsString("http://example.com/my test.asp?first=jane&last=doe")
+                );
+            });
+
+            it("decodes UTF-8 escape sequences into non-ASCII characters", () => {
+                let s = new RoString(new BrsString("http://example.com/?bullet=%E2%80%A2"));
+                let decodeUri = s.getMethod("decodeUri");
+                expect(decodeUri).toBeInstanceOf(Callable);
+
+                expect(decodeUri.call(interpreter)).toEqual(
+                    new BrsString("http://example.com/?bullet=•")
+                );
+            });
+        });
+
+        describe("encodeUriComponent", () => {
+            it("encodes the string for use as a URI component", () => {
+                let s = new RoString(
+                    new BrsString("http://example.com/my test.asp?first=jane&last=doe")
+                );
+                let encodeUriComponent = s.getMethod("encodeUriComponent");
+                expect(encodeUriComponent).toBeInstanceOf(Callable);
+
+                expect(encodeUriComponent.call(interpreter)).toEqual(
+                    new BrsString(
+                        "http%3A%2F%2Fexample.com%2Fmy%20test.asp%3Ffirst%3Djane%26last%3Ddoe"
+                    )
+                );
+            });
+
+            it("encodes non-ascii strings as UTF-8 escape sequences", () => {
+                let s = new RoString(new BrsString("http://example.com/?bullet=•"));
+                let encodeUriComponent = s.getMethod("encodeUriComponent");
+                expect(encodeUriComponent).toBeInstanceOf(Callable);
+
+                expect(encodeUriComponent.call(interpreter)).toEqual(
+                    new BrsString("http%3A%2F%2Fexample.com%2F%3Fbullet%3D%E2%80%A2")
+                );
+            });
+        });
+
+        describe("decodeUriComponent", () => {
+            it("decodes an encoded URI component to a readable string", () => {
+                let s = new RoString(
+                    new BrsString(
+                        "http%3A%2F%2Fexample.com%2Fmy%20test.asp%3Ffirst%3Djane%26last%3Ddoe"
+                    )
+                );
+                let decodeUriComponent = s.getMethod("decodeUriComponent");
+                expect(decodeUriComponent).toBeInstanceOf(Callable);
+
+                expect(decodeUriComponent.call(interpreter)).toEqual(
+                    new BrsString("http://example.com/my test.asp?first=jane&last=doe")
+                );
+            });
+
+            it("decodes UTF-8 escape sequences into non-ASCII characters", () => {
+                let s = new RoString(
+                    new BrsString("http%3A%2F%2Fexample.com%2F%3Fbullet%3D%E2%80%A2")
+                );
+                let decodeUriComponent = s.getMethod("decodeUriComponent");
+                expect(decodeUriComponent).toBeInstanceOf(Callable);
+
+                expect(decodeUriComponent.call(interpreter)).toEqual(
+                    new BrsString("http://example.com/?bullet=•")
+                );
+            });
+        });
+    });
+});
diff --git a/test/e2e/BrsComponents.test.js b/test/e2e/BrsComponents.test.js
index c57dce7..84e1b28 100644
--- a/test/e2e/BrsComponents.test.js
+++ b/test/e2e/BrsComponents.test.js
@@ -104,4 +104,17 @@ describe("end to end brightscript functions", () => {
             " ]",
         ]);
     });
+
+    test("components/roString.brs", async () => {
+        await execute([resourceFile("components", "roString.brs")], outputStreams);
+
+        expect(allArgs(outputStreams.stderr.write)).toEqual([]);
+        expect(allArgs(outputStreams.stdout.write).filter(arg => arg !== "\n")).toEqual([
+            "true", // comparison
+            "5", // length
+            "b", // split("/")[1]
+            "%F0%9F%90%B6", // dog emoji, uri-encoded
+            "🐶", // uri-encoded dog emoji, decoded
+        ]);
+    });
 });
diff --git a/test/e2e/resources/components/roString.brs b/test/e2e/resources/components/roString.brs
new file mode 100644
index 0000000..f5a42a5
--- /dev/null
+++ b/test/e2e/resources/components/roString.brs
@@ -0,0 +1,21 @@
+sub main()
+    ' direct creation
+    r = createObject("RoString", "foo")
+
+    s = "bar"
+
+    r.setString("boo!", 1)
+    r.appendString("ar", 10)
+
+    ' comparisons
+    print r = s ' => true
+
+    ' autoboxing
+    t = "a/b/c"
+    print t.len() ' => 5
+    print t.split("/")[1] ' => b
+
+    u = "🐶"
+    print u.encodeUriComponent() ' => %F0%9F%90%B6
+    print "%F0%9F%90%B6".decodeUriComponent() ' => 🐶
+end sub
\ No newline at end of file
diff --git a/test/interpreter/Comparison.test.js b/test/interpreter/Comparison.test.js
index e9c7ced..7413af5 100644
--- a/test/interpreter/Comparison.test.js
+++ b/test/interpreter/Comparison.test.js
@@ -12,6 +12,7 @@ const {
     RoArray,
     BrsInvalid,
     RoAssociativeArray,
+    RoString,
 } = brs.types;
 
 let interpreter;
@@ -164,6 +165,7 @@ describe("interpreter comparisons", () => {
     describe("disallowed mixed-type comparisons", () => {
         let int32 = new Int32(2);
         let str = new BrsString("two");
+        let rostr = new RoString(str);
         let int64 = new Int64(2);
 
         // due to a combinatoric explosion of LHS-RHS-operator pairs, just test a representative
@@ -193,13 +195,13 @@ describe("interpreter comparisons", () => {
             ]);
         });
 
-        test("string and 64-bit int", () => {
-            let less = binary(str, Lexeme.Less, int64);
-            let lessEqual = binary(str, Lexeme.LessEqual, int64);
-            let greater = binary(str, Lexeme.Greater, int64);
-            let greaterEqual = binary(str, Lexeme.GreaterEqual, int64);
-            let equal = binary(str, Lexeme.Equal, int64);
-            let notEqual = binary(str, Lexeme.LessGreater, int64);
+        test("roString and 64-bit int", () => {
+            let less = binary(rostr, Lexeme.Less, int64);
+            let lessEqual = binary(rostr, Lexeme.LessEqual, int64);
+            let greater = binary(rostr, Lexeme.Greater, int64);
+            let greaterEqual = binary(rostr, Lexeme.GreaterEqual, int64);
+            let equal = binary(rostr, Lexeme.Equal, int64);
+            let notEqual = binary(rostr, Lexeme.LessGreater, int64);
             let results = interpreter.exec([
                 less,
                 lessEqual,