Repository: sjbarag/brs
Pull request #620 (2021-01-27 13:06:40)

fix(interp): Coerce function arguments and variable assignments into desired types
#609
## While running on interpreter:
<img width="377" alt="Screenshot 2021-01-27 at 19 08 40" src="https://user-images.githubusercontent.com/26671643/106027033-2af50a00-60d3-11eb-924c-f74ba3248b51.png">

## On Roku:
![Screenshot 2021-01-27 at 19 11 57](https://user-images.githubusercontent.com/26671643/106027420-8cb57400-60d3-11eb-8d95-0bb732fafcb7.png)



vbuchii: Hi, @sjbarag 
I've added typecasting but we have different behavior as on Roku. I'm not very familiar with JS and would like to get your feedback on what we should do with that. I skipped a part of the tests and left comments above them. Also, you can find screenshots attached to the PR's description. 
Thanks
sjbarag: Thanks for getting this started, @vbuchii!  The implementation looks pretty close, so I'm going to pick it up from here 😃 I'll push some commits to this PR, so don't panic if you see some notifications in your email.

Patch:
diff --git a/src/brsTypes/Callable.ts b/src/brsTypes/Callable.ts
index 7d7a419..7323ebc 100644
--- a/src/brsTypes/Callable.ts
+++ b/src/brsTypes/Callable.ts
@@ -7,6 +7,7 @@ import { Int32 } from "./Int32";
 import { Float } from "./Float";
 import { Double } from "./Double";
 import { Int64 } from "./Int64";
+import { tryCoerce } from "./coercion";
 
 /** An argument to a BrightScript `function` or `sub`. */
 export interface Argument {
@@ -80,6 +81,10 @@ export type SignatureAndImplementation = {
 };
 
 type SignatureMismatch = AnonymousMismatch | ArgumentMismatch;
+type SignatureSatisfaction = SignatureAndImplementation & {
+    coercedArgs: ReadonlyArray<Brs.BrsType>;
+    mismatches: SignatureMismatch[];
+};
 
 /** A mismatch between a BrightScript function's signature and its received arguments. */
 export interface AnonymousMismatch {
@@ -114,9 +119,9 @@ export enum MismatchReason {
 }
 
 /** A BrightScript function's signature, paired with a set of detected signature mismatches. */
-export type SignatureAndMismatches = {
-    /** A BrightScript function's signature. */
-    signature: Signature;
+export type SignatureAndMismatches = SignatureAndImplementation & {
+    /** A copy of the provided arguments, coerced into `signature`'s types if possible. */
+    coercedArgs?: ReadonlyArray<Brs.BrsType>;
     /** The set of mismatches between `signature` and the detected mismatches. */
     mismatches: SignatureMismatch[];
 };
@@ -160,7 +165,7 @@ export class Callable implements Brs.BrsValue {
 
         let { signature, impl } = satisfiedSignature;
 
-        let mutableArgs = args.slice();
+        let mutableArgs = [...satisfiedSignature.coercedArgs];
 
         return interpreter.inSubEnv((subInterpreter) => {
             // first, we need to evaluate all of the parameter default values
@@ -217,91 +222,82 @@ export class Callable implements Brs.BrsValue {
         return this.name || "";
     }
 
-    getFirstSatisfiedSignature(args: Brs.BrsType[]): SignatureAndImplementation | undefined {
-        return this.signatures.filter(
-            (sigAndImpl) => this.getSignatureMismatches(sigAndImpl.signature, args).length === 0
-        )[0];
+    /**
+     * Attempts to satisfy each signature of this Callable using the provided arguments, coercing arguments into other
+     * types where necessary and possible, returning the first satisfied signature and the arguments.
+     * @param args - the arguments to satisfy this Callable with
+     * @returns the signature, implementation, type mismatches, and coerced arguments for the first encountered
+     *          signature satisfied by the provide arguments.
+     */
+    getFirstSatisfiedSignature(args: Brs.BrsType[]): SignatureSatisfaction | undefined {
+        for (let sigAndImpl of this.signatures) {
+            let satisfaction = this.trySatisfySignature(sigAndImpl, args);
+            if (satisfaction.mismatches.length === 0) {
+                return satisfaction;
+            }
+        }
+        return undefined;
     }
 
-    getAllSignatureMismatches(args: Brs.BrsType[]): SignatureAndMismatches[] {
-        return this.signatures.map((sigAndImpl) => ({
-            signature: sigAndImpl.signature,
-            mismatches: this.getSignatureMismatches(sigAndImpl.signature, args),
-        }));
+    /**
+     * Attempts to satisfy each signature of this Callable using the provided arguments, coercing arguments into other
+     * types where necessary and possible.
+     * @param args - the arguments to satisfy this Callable with
+     * @returns the signature, implementation, type mismatches, and coerced arguments for each signature in this
+     *          Callable.
+     */
+    getAllSignatureMismatches(args: Brs.BrsType[]): SignatureSatisfaction[] {
+        return this.signatures.map((sigAndImpl) => this.trySatisfySignature(sigAndImpl, args));
     }
 
-    private getSignatureMismatches(sig: Signature, args: Brs.BrsType[]): SignatureMismatch[] {
+    private trySatisfySignature(
+        sigAndImpl: SignatureAndImplementation,
+        args: ReadonlyArray<Brs.BrsType>
+    ): SignatureSatisfaction {
+        let { signature, impl } = sigAndImpl;
         let reasons: SignatureMismatch[] = [];
-        let requiredArgCount = sig.args.filter((arg) => !arg.defaultValue).length;
+        let requiredArgCount = signature.args.filter((arg) => !arg.defaultValue).length;
 
         if (args.length < requiredArgCount) {
             reasons.push({
                 reason: MismatchReason.TooFewArguments,
-                expected: sig.args.length.toString(),
+                expected: signature.args.length.toString(),
                 received: args.length.toString(),
             });
-        } else if (args.length > sig.args.length) {
+        } else if (args.length > signature.args.length) {
             reasons.push({
                 reason: MismatchReason.TooManyArguments,
-                expected: sig.args.length.toString(),
+                expected: signature.args.length.toString(),
                 received: args.length.toString(),
             });
         }
 
-        sig.args.slice(0, Math.min(sig.args.length, args.length)).forEach((_value, index) => {
-            let expected = sig.args[index];
-            let received = args[index];
-
-            if (
-                expected.type.kind === Brs.ValueKind.Dynamic ||
-                expected.type.kind === Brs.ValueKind.Object
-            ) {
-                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 === Brs.ValueKind.Double &&
-                received.kind === Brs.ValueKind.Float
-            ) {
-                args[index] = new Double(received.getValue());
-                return;
-            }
-
-            if (
-                expected.type.kind === Brs.ValueKind.Int64 &&
-                received.kind === Brs.ValueKind.Int32
-            ) {
-                args[index] = new Int64(received.getValue());
-                return;
-            }
-
-            if (expected.type.kind !== received.kind) {
-                reasons.push({
-                    reason: MismatchReason.ArgumentTypeMismatch,
-                    expected: Brs.ValueKind.toString(expected.type.kind),
-                    received: Brs.ValueKind.toString(received.kind),
-                    argName: expected.name.text,
-                });
-            }
-        });
+        let coercedArgs = [...args];
+        signature.args
+            .slice(0, Math.min(signature.args.length, args.length))
+            .forEach((_value, index) => {
+                let expected = signature.args[index];
+                let received = args[index];
+
+                let coercedValue = tryCoerce(received, expected.type.kind);
+                if (coercedValue != null) {
+                    coercedArgs[index] = coercedValue;
+                } else {
+                    reasons.push({
+                        reason: MismatchReason.ArgumentTypeMismatch,
+                        expected: Brs.ValueKind.toString(expected.type.kind),
+                        received: Brs.ValueKind.toString(received.kind),
+                        argName: expected.name.text,
+                    });
+                }
+            });
 
-        return reasons;
+        return {
+            signature,
+            impl,
+            coercedArgs,
+            mismatches: reasons,
+        };
     }
 
     /**
diff --git a/src/brsTypes/coercion.ts b/src/brsTypes/coercion.ts
new file mode 100644
index 0000000..324eedd
--- /dev/null
+++ b/src/brsTypes/coercion.ts
@@ -0,0 +1,53 @@
+import type { BrsType } from ".";
+import { ValueKind, PrimitiveKinds, isNumberKind, isBrsNumber, BrsComponent } from ".";
+import { isBoxable, isUnboxable } from "./Boxing";
+import { Int32 } from "./Int32";
+import { Int64 } from "./Int64";
+import { Float } from "./Float";
+import { Double } from "./Double";
+
+export function tryCoerce(value: BrsType, target: ValueKind): BrsType | undefined {
+    if (value.kind === target || target === ValueKind.Dynamic) {
+        // `value` is already the correct type; return it immediately
+        return value;
+    }
+
+    if (value.kind === ValueKind.Uninitialized) {
+        // allow uninitialized values to pass through unmodified, for handling by runtime code
+        return value;
+    }
+
+    if (target === ValueKind.Object) {
+        if (isBoxable(value)) {
+            // boxable types should be promoted to their boxed equivalents whenever possible,
+            // with their boxed equivalents coerced if necessary
+            return tryCoerce(value.box(), target);
+        } else if (value instanceof BrsComponent) {
+            // types that are always objects should be returned unmodified
+            return value;
+        }
+    }
+
+    if (PrimitiveKinds.has(target) && isUnboxable(value)) {
+        // unboxable types should be demoted to their unboxed equivalents whenever possible,
+        // with their unboxed equivalents coerced if necessary
+        return tryCoerce(value.unbox(), target);
+    }
+
+    // numeric types can be cast between each other
+    if (isNumberKind(target) && isBrsNumber(value)) {
+        switch (target) {
+            case ValueKind.Int32:
+                return new Int32(value.getValue());
+            case ValueKind.Int64:
+                return new Int64(value.getValue());
+            case ValueKind.Float:
+                return new Float(value.getValue());
+            case ValueKind.Double:
+                return new Double(value.getValue());
+        }
+    }
+
+    // everything else is uncastable; return `undefined` to represent that
+    return;
+}
diff --git a/src/brsTypes/index.ts b/src/brsTypes/index.ts
index 2019647..8c92327 100644
--- a/src/brsTypes/index.ts
+++ b/src/brsTypes/index.ts
@@ -1,14 +1,5 @@
-import {
-    ValueKind,
-    BrsInvalid,
-    BrsBoolean,
-    BrsString,
-    Uninitialized,
-    Comparable,
-    BrsValue,
-} from "./BrsType";
+import { ValueKind, BrsInvalid, BrsBoolean, BrsString, Uninitialized, BrsValue } from "./BrsType";
 import { RoArray } from "./components/RoArray";
-import { RoDeviceInfo } from "./components/RoDeviceInfo";
 import { RoAssociativeArray } from "./components/RoAssociativeArray";
 import { Int32 } from "./Int32";
 import { Int64 } from "./Int64";
@@ -18,12 +9,6 @@ import { Callable } from "./Callable";
 import { BrsComponent } from "./components/BrsComponent";
 import { RoString } from "./components/RoString";
 import { BrsInterface } from "./BrsInterface";
-import { RoSGNode } from "./components/RoSGNode";
-import { Font } from "./components/Font";
-import { Group } from "./components/Group";
-import { Scene } from "./components/Scene";
-import { MiniKeyboard } from "./components/MiniKeyboard";
-import { TextEditBox } from "./components/TextEditBox";
 
 export * from "./BrsType";
 export * from "./Int32";
@@ -65,6 +50,7 @@ export * from "./components/MarkupGrid";
 export * from "./components/ContentNode";
 export * from "./components/Timer";
 export * from "./components/RoAppInfo";
+export * from "./coercion";
 
 /**
  * Determines whether or not the given value is a number.
@@ -72,17 +58,28 @@ export * from "./components/RoAppInfo";
  * @returns `true` if `value` is a numeric value, otherwise `false`.
  */
 export function isBrsNumber(value: BrsType): value is BrsNumber {
-    switch (value.kind) {
-        case ValueKind.Int32:
-        case ValueKind.Int64:
-        case ValueKind.Float:
-        case ValueKind.Double:
-            return true;
-        default:
-            return false;
-    }
+    return NumberKinds.has(value.kind);
 }
 
+export function isNumberKind(kind: ValueKind): boolean {
+    return NumberKinds.has(kind);
+}
+
+export const NumberKinds = new Set([
+    ValueKind.Int32,
+    ValueKind.Float,
+    ValueKind.Double,
+    ValueKind.Int64,
+]);
+
+export const PrimitiveKinds = new Set([
+    ValueKind.Uninitialized,
+    ValueKind.Invalid,
+    ValueKind.Boolean,
+    ...NumberKinds,
+    ValueKind.String,
+]);
+
 /**
  * Determines whether or not the given value is a string.
  * @param value the BrightScript value in question.
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index 20e513f..064ed5e 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -22,6 +22,7 @@ import {
     BrsNumber,
     Float,
     Double,
+    tryCoerce,
 } from "../brsTypes";
 
 import { Lexeme, Location } from "../lexer";
@@ -458,11 +459,10 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
         };
         let requiredType = typeDesignators[name.charAt(name.length - 1)];
 
-        if (requiredType && requiredType !== value.kind) {
-            if (requiredType === ValueKind.Int64 && value.kind === ValueKind.Int32) {
-                value = new Int64(value.getValue());
-            } else if (requiredType === ValueKind.Double && value.kind === ValueKind.Float) {
-                value = new Double(value.getValue());
+        if (requiredType) {
+            let coercedValue = tryCoerce(value, requiredType);
+            if (coercedValue != null) {
+                value = coercedValue;
             } else {
                 return this.addError(
                     new TypeMismatch({


Test patch:
diff --git a/test/brsTypes/Callable.test.js b/test/brsTypes/Callable.test.js
index e9e35cc..541178c 100644
--- a/test/brsTypes/Callable.test.js
+++ b/test/brsTypes/Callable.test.js
@@ -1,5 +1,6 @@
 const BrsTypes = require("../../lib/brsTypes");
 const { UCase, LCase } = require("../../lib/stdlib");
+const { Interpreter } = require("../../lib/interpreter");
 
 describe("Callable", () => {
     it("is less than nothing", () => {
@@ -129,52 +130,133 @@ 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,
+        it("doesn't mutate args while checking for argument mismatches", () => {
+            let func = new BrsTypes.Callable(
+                "acceptsIntOrFloat",
+                {
+                    signature: {
+                        args: [
+                            new BrsTypes.StdlibArgument("input", BrsTypes.ValueKind.Int32),
+                            new BrsTypes.StdlibArgument("something_else", BrsTypes.ValueKind.Int32),
+                        ],
+                        returns: BrsTypes.Int32,
+                    },
+                    impl: (_interpreter, input) => input,
                 },
-                impl: () => {},
-            });
+                {
+                    signature: {
+                        args: [
+                            new BrsTypes.StdlibArgument("input", BrsTypes.ValueKind.Float),
+                            new BrsTypes.StdlibArgument(
+                                "something_else",
+                                BrsTypes.ValueKind.Boolean
+                            ),
+                        ],
+                        returns: BrsTypes.Float,
+                    },
+                    impl: (_interpreter, input) => input,
+                }
+            );
 
-            expect(
-                hasArgs
-                    .getAllSignatureMismatches([new BrsTypes.Float(1.5)])
-                    .map((mm) => mm.mismatches)[0]
-            ).toEqual([]);
+            let pi = new BrsTypes.Float(3.1415927);
+            let result = func.call(new Interpreter(), pi, BrsTypes.BrsBoolean.False);
+
+            // mutation between checks would cause `input` to be the integer 3 - make sure that doesn't happen
+            expect(result).toEqual(pi);
         });
 
-        it("allows float to be passed to a double argument", () => {
-            const hasArgs = new BrsTypes.Callable("acceptsDouble", {
-                signature: {
-                    args: [new BrsTypes.StdlibArgument("doubleArg", BrsTypes.ValueKind.Double)],
-                    returns: BrsTypes.String,
-                },
-                impl: () => {},
+        describe("coercion", () => {
+            describe("into integer", () => {
+                let fn = new BrsTypes.Callable("acceptsInt", {
+                    signature: {
+                        args: [new BrsTypes.StdlibArgument("input", BrsTypes.ValueKind.Int32)],
+                        returns: BrsTypes.ValueKind.Int32,
+                    },
+                    impl: (_interpreter, input) => input,
+                });
+
+                test.each([
+                    // pairs of [type, input, coerced_value]
+                    ["integer", new BrsTypes.Int32(-5), new BrsTypes.Int32(-5)],
+                    ["float", new BrsTypes.Float(3.14159), new BrsTypes.Int32(3)],
+                    ["double", new BrsTypes.Double(2.71828), new BrsTypes.Int32(2)],
+                    ["longinteger", new BrsTypes.Int64(2147483647119), new BrsTypes.Int32(-881)],
+                ])("from %s to integer", (_type, input, output) => {
+                    expect(fn.call(new Interpreter(), input)).toEqual(output);
+                });
             });
 
-            expect(
-                hasArgs
-                    .getAllSignatureMismatches([new BrsTypes.Float(1.5)])
-                    .map((mm) => mm.mismatches)[0]
-            ).toEqual([]);
-        });
+            describe("into float", () => {
+                let fn = new BrsTypes.Callable("acceptsFloat", {
+                    signature: {
+                        args: [new BrsTypes.StdlibArgument("input", BrsTypes.ValueKind.Float)],
+                        returns: BrsTypes.ValueKind.Float,
+                    },
+                    impl: (_interpreter, input) => input,
+                });
 
-        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: () => {},
+                test.each([
+                    // pairs of [type, input, coerced_value]
+                    ["integer", new BrsTypes.Int32(-5), new BrsTypes.Float(-5)],
+                    ["float", new BrsTypes.Float(3.14159), new BrsTypes.Float(3.14159)],
+                    ["double", new BrsTypes.Double(2.71828), new BrsTypes.Float(2.71828)],
+                    [
+                        "longinteger",
+                        new BrsTypes.Int64(2147483647119),
+                        new BrsTypes.Float(2147483647119),
+                    ],
+                ])("from %s to float", (_type, input, output) => {
+                    expect(fn.call(new Interpreter(), input)).toEqual(output);
+                });
             });
 
-            expect(
-                hasArgs
-                    .getAllSignatureMismatches([new BrsTypes.Int32(4)])
-                    .map((mm) => mm.mismatches)[0]
-            ).toEqual([]);
+            describe("into double", () => {
+                let fn = new BrsTypes.Callable("acceptsDouble", {
+                    signature: {
+                        args: [new BrsTypes.StdlibArgument("input", BrsTypes.ValueKind.Double)],
+                        returns: BrsTypes.ValueKind.Double,
+                    },
+                    impl: (_interpreter, input) => input,
+                });
+
+                test.each([
+                    // pairs of [type, input, coerced_value]
+                    ["integer", new BrsTypes.Int32(-5), new BrsTypes.Double(-5)],
+                    ["float", new BrsTypes.Float(3.14159), new BrsTypes.Double(3.14159)],
+                    ["double", new BrsTypes.Double(2.71828), new BrsTypes.Double(2.71828)],
+                    [
+                        "longinteger",
+                        new BrsTypes.Int64(2147483647119),
+                        new BrsTypes.Double(2147483647119),
+                    ],
+                ])("from %s to double", (_type, input, output) => {
+                    expect(fn.call(new Interpreter(), input)).toEqual(output);
+                });
+            });
+
+            describe("into longinteger", () => {
+                let fn = new BrsTypes.Callable("acceptsLongInteger", {
+                    signature: {
+                        args: [new BrsTypes.StdlibArgument("input", BrsTypes.ValueKind.Int64)],
+                        returns: BrsTypes.ValueKind.Int64,
+                    },
+                    impl: (_interpreter, input) => input,
+                });
+
+                test.each([
+                    // pairs of [type, input, coerced_value]
+                    ["integer", new BrsTypes.Int32(-5), new BrsTypes.Int64(-5)],
+                    ["float", new BrsTypes.Float(3.14159), new BrsTypes.Int64(3)],
+                    ["double", new BrsTypes.Double(2.71828), new BrsTypes.Int64(2)],
+                    [
+                        "longinteger",
+                        new BrsTypes.Int64(2147483647119),
+                        new BrsTypes.Int64(2147483647119),
+                    ],
+                ])("from %s to longinteger", (_type, input, output) => {
+                    expect(fn.call(new Interpreter(), input)).toEqual(output);
+                });
+            });
         });
 
         it("allows any type for dynamic and object", () => {
diff --git a/test/brsTypes/coercion.test.js b/test/brsTypes/coercion.test.js
new file mode 100644
index 0000000..c8e380c
--- /dev/null
+++ b/test/brsTypes/coercion.test.js
@@ -0,0 +1,135 @@
+const { types } = require("../../lib");
+const {
+    tryCoerce,
+    ValueKind,
+    Int32,
+    Int64,
+    Float,
+    Double,
+    BrsBoolean,
+    BrsInvalid,
+    RoAssociativeArray,
+    RoSGNode,
+    BrsString,
+    Uninitialized,
+    roInt,
+    roFloat,
+    roDouble,
+    roLongInteger,
+    RoString,
+    roBoolean,
+} = types;
+
+describe("type coercion", () => {
+    describe("tryCoerce", () => {
+        it("returns input for dynamic target type", () => {
+            let inputs = [
+                new Int32(1234),
+                new Float(2.345),
+                new Double(3.456),
+                new Int64(4567),
+                new BrsString("lorem"),
+                BrsBoolean.False,
+                BrsInvalid.Instance,
+                new RoAssociativeArray([]),
+                new RoSGNode([]),
+            ];
+
+            inputs.forEach((input) => {
+                expect(input).toBeDefined();
+                expect(tryCoerce(input, ValueKind.Dynamic)).toBe(input);
+            });
+        });
+
+        it.each([
+            ["integer", new Int32(1234), ValueKind.Int32],
+            ["float", new Float(2.345), ValueKind.Float],
+            ["double", new Double(3.456), ValueKind.Double],
+            ["longinteger", new Int64(4567), ValueKind.Int64],
+            ["string", new BrsString("lorem"), ValueKind.String],
+            ["boolean", BrsBoolean.False, ValueKind.Boolean],
+            ["invalid", BrsInvalid.Instance, ValueKind.Invalid],
+        ])("returns input for %s target when no coercion needed", (_type, input, target) => {
+            expect(input).toBeDefined();
+            expect(tryCoerce(input, target)).toBe(input);
+        });
+
+        it("returns uninitialized for any target type", () => {
+            expect(tryCoerce(Uninitialized.Instance, ValueKind.String)).toBe(
+                Uninitialized.Instance
+            );
+        });
+
+        describe("boxing", () => {
+            it.each([
+                ["integer", new Int32(1234), roInt],
+                ["float", new Float(2.345), roFloat],
+                ["double", new Double(3.456), roDouble],
+                ["longinteger", new Int64(3.456), roLongInteger],
+                ["string", new BrsString("lorem"), RoString],
+                ["boolean", BrsBoolean.False, roBoolean],
+            ])("boxes primitive %s for object target", (_type, input, ctor) => {
+                let output = tryCoerce(input, ValueKind.Object);
+                expect(output).toBeInstanceOf(ctor);
+                expect(output.getValue()).toEqual(input.box().getValue());
+            });
+        });
+
+        describe("unboxing", () => {
+            it.each([
+                ["integer", new roInt(new Int32(1234)), ValueKind.Int32, Int32],
+                ["float", new roFloat(new Float(2.345)), ValueKind.Float, Float],
+                ["double", new roDouble(new Double(3.456)), ValueKind.Double, Double],
+                ["longinteger", new roLongInteger(new Int64(3.456)), ValueKind.Int64, Int64],
+                ["string", new RoString(new BrsString("lorem")), ValueKind.String, BrsString],
+                ["boolean", new roBoolean(BrsBoolean.False), ValueKind.Boolean, BrsBoolean],
+            ])("boxes %s for object target", (_type, input, target, ctor) => {
+                let output = tryCoerce(input, target);
+                expect(output).toBeInstanceOf(ctor);
+                expect(output.box().getValue()).toEqual(input.getValue());
+            });
+        });
+
+        describe("numeric conversions", () => {
+            it.each([
+                // pairs of [type, input, coerced_value]
+                ["float", new Float(3.14159), new Int32(3)],
+                ["double", new Double(2.71828), new Int32(2)],
+                ["longinteger", new Int64(2147483647119), new Int32(-881)],
+            ])("returns integer for %s target", (_type, input, output) => {
+                expect(tryCoerce(input, ValueKind.Int32)).toEqual(output);
+                expect(tryCoerce(input.box(), ValueKind.Int32)).toEqual(output);
+            });
+
+            it.each([
+                // pairs of [type, input, coerced_value]
+                ["integer", new Int32(-5), new Float(-5)],
+                ["double", new Double(2.71828), new Float(2.71828)],
+                ["longinteger", new Int64(2147483647119), new Float(2147483647119)],
+            ])("returns float for %s target", (_type, input, output) => {
+                expect(tryCoerce(input, ValueKind.Float)).toEqual(output);
+                expect(tryCoerce(input.box(), ValueKind.Float)).toEqual(output);
+            });
+
+            it.each([
+                // pairs of [type, input, coerced_value]
+                ["integer", new Int32(-5), new Double(-5)],
+                ["float", new Float(3.14159), new Double(3.14159)],
+                ["longinteger", new Int64(2147483647119), new Double(2147483647119)],
+            ])("returns double for %s target", (_type, input, output) => {
+                expect(tryCoerce(input, ValueKind.Double)).toEqual(output);
+                expect(tryCoerce(input.box(), ValueKind.Double)).toEqual(output);
+            });
+
+            it.each([
+                // pairs of [type, input, coerced_value]
+                ["integer", new Int32(-5), new Int64(-5)],
+                ["float", new Float(3.14159), new Int64(3)],
+                ["double", new Double(2.71828), new Int64(2)],
+            ])("returns longinteger for %s target", (_type, input, output) => {
+                expect(tryCoerce(input, ValueKind.Int64)).toEqual(output);
+                expect(tryCoerce(input.box(), ValueKind.Int64)).toEqual(output);
+            });
+        });
+    });
+});
diff --git a/test/brsTypes/components/RoSGNode.test.js b/test/brsTypes/components/RoSGNode.test.js
index 499081b..bbd0cb9 100644
--- a/test/brsTypes/components/RoSGNode.test.js
+++ b/test/brsTypes/components/RoSGNode.test.js
@@ -478,7 +478,7 @@ describe("RoSGNode", () => {
 
             it("only adds fields if passed as an associative array", () => {
                 let node = new RoSGNode([]);
-                let fields = "non associative array";
+                let fields = new BrsString("non associative array");
 
                 let addFields = node.getMethod("addfields");
 
diff --git a/test/e2e/TypeChecking.test.js b/test/e2e/TypeChecking.test.js
index 71d6c6a..57625b0 100644
--- a/test/e2e/TypeChecking.test.js
+++ b/test/e2e/TypeChecking.test.js
@@ -11,11 +11,11 @@ describe("function argument type checking", () => {
     });
 
     afterEach(() => {
-        stderr.mockClear();
+        jest.resetAllMocks();
     });
 
     afterAll(() => {
-        stderr.mockRestore();
+        jest.restoreAllMocks();
     });
 
     it("errors when too few args are passed", () => {
@@ -71,4 +71,81 @@ describe("function argument type checking", () => {
             );
         });
     });
+
+    it("coerces function call arguments where possible", async () => {
+        await execute([resourceFile("type-checking", "argument-type-coercion.brs")], outputStreams);
+        expect(
+            allArgs(outputStreams.stdout.write)
+                .join("")
+                .split("\n")
+                .filter((s) => !!s)
+        ).toEqual([
+            "calling '[Function acceptsInteger]' with argument of type 'double' with value: 2.71828",
+            "received: 2",
+            "calling '[Function acceptsFloat]' with argument of type 'double' with value: 2.71828",
+            "received: 2.71828",
+            "calling '[Function acceptsDouble]' with argument of type 'double' with value: 2.71828",
+            "received: 2.71828",
+            "calling '[Function acceptsLongInt]' with argument of type 'double' with value: 2.71828",
+            "received: 2",
+            "calling '[Function acceptsInteger]' with argument of type 'float' with value: 3.14159",
+            "received: 3",
+            "calling '[Function acceptsFloat]' with argument of type 'float' with value: 3.14159",
+            "received: 3.14159",
+            "calling '[Function acceptsDouble]' with argument of type 'float' with value: 3.14159",
+            "received: 3.14159",
+            "calling '[Function acceptsLongInt]' with argument of type 'float' with value: 3.14159",
+            "received: 3",
+            "calling '[Function acceptsInteger]' with argument of type 'integer' with value: 13",
+            "received: 13",
+            "calling '[Function acceptsFloat]' with argument of type 'integer' with value: 13",
+            "received: 13",
+            "calling '[Function acceptsDouble]' with argument of type 'integer' with value: 13",
+            "received: 13",
+            "calling '[Function acceptsLongInt]' with argument of type 'integer' with value: 13",
+            "received: 13",
+            "calling '[Function acceptsInteger]' with argument of type 'longinteger' with value: 2147483647119",
+            "received: -881",
+            "calling '[Function acceptsFloat]' with argument of type 'longinteger' with value: 2147483647119",
+            "received: 2147484000000",
+            "calling '[Function acceptsDouble]' with argument of type 'longinteger' with value: 2147483647119",
+            "received: 2147483647119",
+            "calling '[Function acceptsLongInt]' with argument of type 'longinteger' with value: 2147483647119",
+            "received: 2147483647119",
+        ]);
+    });
+
+    it("coerces assignment RHS values where possible", async () => {
+        await execute(
+            [resourceFile("type-checking", "assignment-type-coercion.brs")],
+            outputStreams
+        );
+        expect(
+            allArgs(outputStreams.stdout.write)
+                .join("")
+                .split("\n")
+                .filter((s) => !!s)
+        ).toEqual([
+            "assigning RHS of type 'double' with value: 2.71828",
+            "integer% = 2",
+            "float! = 2.71828",
+            "double# = 2.71828",
+            "longinteger& = 2",
+            "assigning RHS of type 'float' with value: 3.14159",
+            "integer% = 3",
+            "float! = 3.14159",
+            "double# = 3.14159",
+            "longinteger& = 3",
+            "assigning RHS of type 'integer' with value: 13",
+            "integer% = 13",
+            "float! = 13",
+            "double# = 13",
+            "longinteger& = 13",
+            "assigning RHS of type 'longinteger' with value: 2147483647119",
+            "integer% = -881",
+            "float! = 2147484000000",
+            "double# = 2147483647119",
+            "longinteger& = 2147483647119",
+        ]);
+    });
 });
diff --git a/test/e2e/resources/type-checking/argument-type-coercion.brs b/test/e2e/resources/type-checking/argument-type-coercion.brs
new file mode 100644
index 0000000..2f202e1
--- /dev/null
+++ b/test/e2e/resources/type-checking/argument-type-coercion.brs
@@ -0,0 +1,34 @@
+sub acceptsInteger(t as integer)
+    print "received: " t.toStr()
+end sub
+
+sub acceptsFloat(t as float)
+    print "received: " t.toStr()
+end sub
+
+sub acceptsDouble(t as double)
+    print "received: " t.toStr()
+end sub
+
+sub acceptsLongInt(t as longinteger)
+    print "received: " t.toStr()
+end sub
+
+function main()
+    values = {
+        "integer": 13,
+        "float": 3.14159!,
+        "double": 2.71828#,
+        "longinteger": 2147483647119&,
+    }
+
+    for each kv in values.items()
+        _type = kv.key
+        value = kv.value
+
+        for each f in [acceptsInteger, acceptsFloat, acceptsDouble, acceptsLongInt]
+            print "calling '" f "' with argument of type '" _type "' with value: " value.toStr()
+            f(value)
+        end for
+    end for
+end function
diff --git a/test/e2e/resources/type-checking/assignment-type-coercion.brs b/test/e2e/resources/type-checking/assignment-type-coercion.brs
new file mode 100644
index 0000000..9560a39
--- /dev/null
+++ b/test/e2e/resources/type-checking/assignment-type-coercion.brs
@@ -0,0 +1,25 @@
+sub main()
+    values = {
+        "integer": 13,
+        "float": 3.14159!,
+        "double": 2.71828#,
+        "longinteger": 2147483647119&,
+    }
+
+    for each kv in values.items()
+        _type = kv.key
+        value = kv.value
+
+        print "assigning RHS of type '" _type "' with value: " value.toStr()
+
+        integer% = value
+        float! = value
+        double# = value
+        longinteger& = value
+
+        print "integer% = " integer%
+        print "float! = " float!
+        print "double# = " double#
+        print "longinteger& = " longinteger&
+    end for
+end sub
diff --git a/test/interpreter/Variables.test.js b/test/interpreter/Variables.test.js
index a8a2198..db236da 100644
--- a/test/interpreter/Variables.test.js
+++ b/test/interpreter/Variables.test.js
@@ -59,9 +59,7 @@ describe("interpreter variables", () => {
             new Stmt.Assignment(tokens, identifier("int32%"), new Expr.Literal(new Int32(1))),
             new Stmt.Assignment(tokens, identifier("float!"), new Expr.Literal(new Float(2))),
             new Stmt.Assignment(tokens, identifier("double#"), new Expr.Literal(new Double(3))),
-            new Stmt.Assignment(tokens, identifier("double#"), new Expr.Literal(new Float(3))),
             new Stmt.Assignment(tokens, identifier("int64&"), new Expr.Literal(new Int64(4))),
-            new Stmt.Assignment(tokens, identifier("int64&"), new Expr.Literal(new Int32(4))),
         ];
 
         expect(() => interpreter.exec(assign)).not.toThrow();
@@ -80,6 +78,72 @@ describe("interpreter variables", () => {
         ]);
     });
 
+    describe("type coercion", () => {
+        describe("into integer LHS", () => {
+            test.each([
+                // pairs of [type, rhs, lhs_result]
+                ["integer", new Int32(-5), new Int32(-5)],
+                ["float", new Float(3.14159), new Int32(3)],
+                ["double", new Double(2.71828), new Int32(2)],
+                ["longinteger", new Int64(2147483647119), new Int32(-881)],
+            ])("from %s to integer", (_type, rhs, lhs_result) => {
+                let assign = [
+                    new Stmt.Assignment(tokens, identifier("int32%"), new Expr.Literal(rhs)),
+                ];
+                expect(() => interpreter.exec(assign)).not.toThrow();
+                expect(interpreter.environment.get(identifier("int32%"))).toEqual(lhs_result);
+            });
+        });
+
+        describe("into float LHS", () => {
+            test.each([
+                // pairs of [type, rhs, lhs_result]
+                ["integer", new Int32(-5), new Float(-5)],
+                ["float", new Float(3.14159), new Float(3.14159)],
+                ["double", new Double(2.71828), new Float(2.71828)],
+                ["longinteger", new Int64(2147483647119), new Float(2147483647119)],
+            ])("from %s to integer", (_type, rhs, lhs_result) => {
+                let assign = [
+                    new Stmt.Assignment(tokens, identifier("float!"), new Expr.Literal(rhs)),
+                ];
+                expect(() => interpreter.exec(assign)).not.toThrow();
+                expect(interpreter.environment.get(identifier("float!"))).toEqual(lhs_result);
+            });
+        });
+
+        describe("into double LHS", () => {
+            test.each([
+                // pairs of [type, rhs, lhs_result]
+                ["integer", new Int32(-5), new Double(-5)],
+                ["float", new Float(3.14159), new Double(3.14159)],
+                ["double", new Double(2.71828), new Double(2.71828)],
+                ["longinteger", new Int64(2147483647119), new Double(2147483647119)],
+            ])("from %s to integer", (_type, rhs, lhs_result) => {
+                let assign = [
+                    new Stmt.Assignment(tokens, identifier("double#"), new Expr.Literal(rhs)),
+                ];
+                expect(() => interpreter.exec(assign)).not.toThrow();
+                expect(interpreter.environment.get(identifier("double#"))).toEqual(lhs_result);
+            });
+        });
+
+        describe("into longinteger LHS", () => {
+            test.each([
+                // pairs of [type, rhs, lhs_result]
+                ["integer", new Int32(-5), new Int64(-5)],
+                ["float", new Float(3.14159), new Int64(3)],
+                ["double", new Double(2.71828), new Int64(2)],
+                ["longinteger", new Int64(2147483647119), new Int64(2147483647119)],
+            ])("from %s to integer", (_type, rhs, lhs_result) => {
+                let assign = [
+                    new Stmt.Assignment(tokens, identifier("longint&"), new Expr.Literal(rhs)),
+                ];
+                expect(() => interpreter.exec(assign)).not.toThrow();
+                expect(interpreter.environment.get(identifier("longint&"))).toEqual(lhs_result);
+            });
+        });
+    });
+
     describe("type mismatch errors", () => {
         let str = new BrsString("foo");
         let int32 = new Int32(1);
@@ -94,19 +158,19 @@ describe("interpreter variables", () => {
             },
             {
                 lhs: "int32%",
-                values: [str, float, double, int64],
+                values: [str],
             },
             {
                 lhs: "float!",
-                values: [str, int32, double, int64],
+                values: [str],
             },
             {
                 lhs: "double#",
-                values: [str, int32, int64],
+                values: [str],
             },
             {
                 lhs: "int64&",
-                values: [str, float, double],
+                values: [str],
             },
         ].forEach(({ lhs, values }) => {
             test(lhs, () => {