diff --git a/src/brsTypes/components/ContentNode.ts b/src/brsTypes/components/ContentNode.ts index 6e20d4d2..b8505dab 100644 --- a/src/brsTypes/components/ContentNode.ts +++ b/src/brsTypes/components/ContentNode.ts @@ -1,11 +1,10 @@ import { FieldModel, Field, RoSGNode } from "./RoSGNode"; -import { BrsType } from ".."; +import { BrsType, toAssociativeArray } from ".."; import { ValueKind, BrsString, BrsBoolean } from "../BrsType"; import { Interpreter } from "../../interpreter"; import { Int32 } from "../Int32"; import { Callable, StdlibArgument } from "../Callable"; import { RoArray } from "./RoArray"; -import { RoAssociativeArray } from "./RoAssociativeArray"; export class ContentNode extends RoSGNode { readonly defaultFields: FieldModel[] = [ @@ -208,16 +207,7 @@ export class ContentNode extends RoSGNode { impl: (interpreter: Interpreter) => { return new RoArray( this.getElements().map((key: BrsString) => { - return new RoAssociativeArray([ - { - name: new BrsString("key"), - value: key, - }, - { - name: new BrsString("value"), - value: this.get(key), - }, - ]); + return toAssociativeArray({ key: key, value: this.get(key) }); }) ); }, diff --git a/src/brsTypes/components/RoAssociativeArray.ts b/src/brsTypes/components/RoAssociativeArray.ts index 0e26e089..8a563cb3 100644 --- a/src/brsTypes/components/RoAssociativeArray.ts +++ b/src/brsTypes/components/RoAssociativeArray.ts @@ -191,7 +191,9 @@ export class RoAssociativeArray extends BrsComponent implements BrsValue, BrsIte let lKey = str.value.toLowerCase(); if (this.modeCaseSensitive) { let keySet = this.keyMap.get(lKey); - keySet?.delete(key); + if (keySet && key) { + keySet.delete(key); + } if (keySet?.size === 0) { this.keyMap.delete(lKey); } diff --git a/src/brsTypes/components/RoDeviceInfo.ts b/src/brsTypes/components/RoDeviceInfo.ts index 4c3b2674..7da85374 100644 --- a/src/brsTypes/components/RoDeviceInfo.ts +++ b/src/brsTypes/components/RoDeviceInfo.ts @@ -1,5 +1,5 @@ import { BrsValue, ValueKind, BrsString, BrsBoolean, BrsInvalid } from "../BrsType"; -import { BrsType, Int32, RoArray } from ".."; +import { BrsType, Int32, RoArray, toAssociativeArray } from ".."; import { BrsComponent } from "./BrsComponent"; import { Callable, StdlibArgument } from "../Callable"; import { RoAssociativeArray, AAMember } from "./RoAssociativeArray"; @@ -535,12 +535,7 @@ export class RoDeviceInfo extends BrsComponent implements BrsValue { returns: ValueKind.Object, }, impl: (_interpreter, videoFormat: ValueKind.Object) => { - let result = new Array(); - - result.push({ name: new BrsString("result"), value: BrsBoolean.True }); - result.push({ name: new BrsString("codec"), value: new BrsString("mpeg4 avc") }); - - return new RoAssociativeArray(result); + return toAssociativeArray({ result: true, codec: "mpeg4 avc" }); }, }); @@ -601,11 +596,7 @@ export class RoDeviceInfo extends BrsComponent implements BrsValue { returns: ValueKind.Object, }, impl: (_interpreter, audioFormat: ValueKind.Object) => { - let result = new Array(); - - result.push({ name: new BrsString("result"), value: BrsBoolean.True }); - - return new RoAssociativeArray(result); + return toAssociativeArray({ result: true }); }, }); diff --git a/src/brsTypes/components/RoPath.ts b/src/brsTypes/components/RoPath.ts index a14577f1..683c776c 100644 --- a/src/brsTypes/components/RoPath.ts +++ b/src/brsTypes/components/RoPath.ts @@ -1,6 +1,6 @@ import { BrsValue, ValueKind, BrsString, BrsBoolean, BrsInvalid, Comparable } from "../BrsType"; import { BrsComponent } from "./BrsComponent"; -import { BrsType, RoAssociativeArray, AAMember, isStringComp } from ".."; +import { BrsType, RoAssociativeArray, isStringComp, toAssociativeArray } from ".."; import { Callable, StdlibArgument } from "../Callable"; import { Interpreter } from "../../interpreter"; import path from "path"; @@ -106,8 +106,8 @@ export class RoPath extends BrsComponent implements BrsValue, Comparable { }, }); - /** Returns an roAssociativeArrays containing the significant elements of the path */ - private split = new Callable("split", { + /** Returns an roAssociativeArray containing the significant elements of the path */ + private readonly split = new Callable("split", { signature: { args: [], returns: ValueKind.Object, @@ -116,28 +116,14 @@ export class RoPath extends BrsComponent implements BrsValue, Comparable { if (this.fullPath === "") { return new RoAssociativeArray([]); } - const parts = new Array(); - parts.push({ - name: new BrsString("basename"), - value: new BrsString(this.parsedPath.name), - }); - parts.push({ - name: new BrsString("extension"), - value: new BrsString(this.parsedPath.ext), - }); - parts.push({ - name: new BrsString("filename"), - value: new BrsString(this.parsedPath.base), - }); - parts.push({ - name: new BrsString("parent"), - value: new BrsString(this.getParentPart()), - }); - parts.push({ - name: new BrsString("phy"), - value: new BrsString(this.parsedUrl.protocol), - }); - return new RoAssociativeArray(parts); + const parts = { + basename: this.parsedPath.name, + extension: this.parsedPath.ext, + filename: this.parsedPath.base, + parent: this.getParentPart(), + phy: this.parsedUrl.protocol, + }; + return toAssociativeArray(parts); }, }); diff --git a/src/brsTypes/components/RoSGNode.ts b/src/brsTypes/components/RoSGNode.ts index 19e40f64..072afa12 100644 --- a/src/brsTypes/components/RoSGNode.ts +++ b/src/brsTypes/components/RoSGNode.ts @@ -10,7 +10,7 @@ import { } from "../BrsType"; import { RoSGNodeEvent } from "./RoSGNodeEvent"; import { BrsComponent, BrsIterable } from "./BrsComponent"; -import { BrsType, isBrsNumber } from ".."; +import { BrsType, isBrsNumber, toAssociativeArray } from ".."; import { Callable, StdlibArgument } from "../Callable"; import { Interpreter } from "../../interpreter"; import { Int32 } from "../Int32"; @@ -807,16 +807,7 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable { impl: (interpreter: Interpreter) => { return new RoArray( this.getElements().map((key: BrsString) => { - return new RoAssociativeArray([ - { - name: new BrsString("key"), - value: key, - }, - { - name: new BrsString("value"), - value: this.get(key), - }, - ]); + return toAssociativeArray({ key: key, value: this.get(key) }); }) ); }, @@ -1532,13 +1523,7 @@ export class RoSGNode extends BrsComponent implements BrsValue, BrsIterable { returns: ValueKind.Dynamic, }, impl: (interpreter: Interpreter) => { - const zeroValue = new Int32(0); - return new RoAssociativeArray([ - { name: new BrsString("x"), value: zeroValue }, - { name: new BrsString("y"), value: zeroValue }, - { name: new BrsString("height"), value: zeroValue }, - { name: new BrsString("width"), value: zeroValue }, - ]); + return toAssociativeArray({ x: 0, y: 0, width: 0, height: 0 }); }, }); diff --git a/src/brsTypes/index.ts b/src/brsTypes/index.ts index f98f0efb..c4fb6ddc 100644 --- a/src/brsTypes/index.ts +++ b/src/brsTypes/index.ts @@ -178,6 +178,19 @@ export type AllComponents = { kind: ValueKind.Object } & BrsComponent & BrsValue /** The set of all supported types in BrightScript. */ export type BrsType = BrsPrimitive | Iterable | Callable | AllComponents | Uninitialized; +// Function to check if the value is a BrightScript Type +export function isBrsType(value: any): value is BrsType { + return ( + isBrsBoolean(value) || + isBrsString(value) || + isBrsNumber(value) || + value === BrsInvalid.Instance || + value instanceof BrsComponent || + value instanceof Callable || + value instanceof Uninitialized + ); +} + /** The valid ISO Date formats for roDateTime and roTimeSpan parsing */ export const ValidDateFormats = [ "YYYY-MM-DDTHH:mm:ss.SSS[Z]", @@ -196,3 +209,68 @@ export const ValidDateFormats = [ "YYYY[Z]", "YYYY", ]; + +/** + * Converts a JavaScript object or Map to a RoAssociativeArray, converting each property or entry to the corresponding BrightScript type. + * @param input The JavaScript object or Map to convert. + * @returns A RoAssociativeArray with the converted properties or entries. + */ +export function toAssociativeArray(input: any): RoAssociativeArray { + const associativeArray = new RoAssociativeArray([]); + if (input instanceof Map) { + input.forEach((value, key) => { + let brsKey: BrsString; + if (typeof key === "string") { + brsKey = new BrsString(key); + } else { + throw new Error(`Unsupported key type: ${typeof key}`); + } + associativeArray.set(brsKey, brsValueOf(value), true); + }); + } else if (typeof input === "object" && input !== null) { + for (const key in input) { + if (input.hasOwnProperty(key)) { + const brsKey = new BrsString(key); + associativeArray.set(brsKey, brsValueOf(input[key]), true); + } + } + } else { + throw new Error(`Unsupported input type: ${typeof input}`); + } + return associativeArray; +} + +/** + * Converts a value to its representation as a BrsType. If no such + * representation is possible, throws an Error. + * @param {any} x Some value. + * @return {BrsType} The BrsType representation of `x`. + * @throws {Error} If `x` cannot be represented as a BrsType. + */ +export function brsValueOf(x: any): BrsType { + if (x === null || x === undefined || Number.isNaN(x)) { + return BrsInvalid.Instance; + } + const maxInt = 0x80000000; + const t: string = typeof x; + switch (t) { + case "boolean": + return BrsBoolean.from(x); + case "string": + return new BrsString(x); + case "number": + if (Number.isInteger(x)) { + return x >= -maxInt && x < maxInt ? new Int32(x) : new Int64(x); + } + return x >= -3.4e38 && x <= 3.4e38 ? new Float(x) : new Double(x); + case "object": + if (isBrsType(x)) { + return x; + } else if (Array.isArray(x)) { + return new RoArray(x.map(brsValueOf)); + } + return toAssociativeArray(x); + default: + throw new Error(`brsValueOf not implemented for: ${x} <${t}>`); + } +} diff --git a/src/extensions/index.ts b/src/extensions/index.ts index 14b5c9da..865c8b0c 100644 --- a/src/extensions/index.ts +++ b/src/extensions/index.ts @@ -1,4 +1,4 @@ -import { RoAssociativeArray, BrsString, mGlobal } from "../brsTypes"; +import { RoAssociativeArray, BrsString, mGlobal, toAssociativeArray } from "../brsTypes"; import { mockComponent } from "./mockComponent"; import { mockFunction } from "./mockFunction"; import { mockComponentPartial } from "./mockComponentPartial"; @@ -14,22 +14,22 @@ import { import { triggerKeyEvent } from "./triggerKeyEvent"; import { GetStackTrace } from "./GetStackTrace"; -export const _brs_ = new RoAssociativeArray([ - { name: new BrsString("mockComponent"), value: mockComponent }, - { name: new BrsString("mockFunction"), value: mockFunction }, - { name: new BrsString("mockComponentPartial"), value: mockComponentPartial }, - { name: new BrsString("resetMocks"), value: resetMocks }, - { name: new BrsString("resetMockComponents"), value: resetMockComponents }, - { name: new BrsString("resetMockComponent"), value: resetMockComponent }, - { name: new BrsString("resetMockFunctions"), value: resetMockFunctions }, - { name: new BrsString("resetMockFunction"), value: resetMockFunction }, - { name: new BrsString("runInScope"), value: RunInScope }, - { name: new BrsString("process"), value: Process }, - { name: new BrsString("global"), value: mGlobal }, - { name: new BrsString("testData"), value: new RoAssociativeArray([]) }, - { name: new BrsString("triggerKeyEvent"), value: triggerKeyEvent }, - { name: new BrsString("getStackTrace"), value: GetStackTrace }, -]); +export const _brs_ = toAssociativeArray({ + mockComponent: mockComponent, + mockFunction: mockFunction, + mockComponentPartial: mockComponentPartial, + resetMocks: resetMocks, + resetMockComponents: resetMockComponents, + resetMockComponent: resetMockComponent, + resetMockFunctions: resetMockFunctions, + resetMockFunction: resetMockFunction, + runInScope: RunInScope, + process: Process, + global: mGlobal, + testData: new RoAssociativeArray([]), + triggerKeyEvent: triggerKeyEvent, + getStackTrace: GetStackTrace, +}); /** resets _brs_.testData values to `{}` in brightscript representation */ export function resetTestData() { diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 40a24b2f..14290c9b 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -29,6 +29,7 @@ import { RoList, RoXMLElement, RoSGNode, + toAssociativeArray, } from "../brsTypes"; import { Lexeme, Location } from "../lexer"; @@ -1054,12 +1055,12 @@ export class Interpreter implements Expr.Visitor, Stmt.Visitor if (err instanceof RuntimeError) { errDetail = err.errorDetail; } - const errorAA = new RoAssociativeArray([ - { name: new BrsString("backtrace"), value: btArray }, - { name: new BrsString("message"), value: new BrsString(errMessage) }, - { name: new BrsString("number"), value: new Int32(errDetail.errno) }, - { name: new BrsString("rethrown"), value: BrsBoolean.False }, - ]); + const errorAA = toAssociativeArray({ + backtrace: btArray, + message: errMessage, + number: errDetail.errno, + rethrown: false, + }); if (err instanceof RuntimeError && err.extraFields?.size) { for (const [key, value] of err.extraFields) { errorAA.set(new BrsString(key), value); @@ -1845,27 +1846,20 @@ export class Interpreter implements Expr.Visitor, Stmt.Visitor const btArray: BrsType[] = []; for (let index = backTrace.length - 1; index >= 0; index--) { const func = backTrace[index]; - if (func.signature) { - const kind = ValueKind.toString(func.signature.returns); - let args = ""; - func.signature.args.forEach((arg) => { - args += args !== "" ? "," : ""; - args += `${arg.name.text} As ${ValueKind.toString(arg.type.kind)}`; - }); - const funcSignature = `${func.functionName}(${args}) As ${kind}`; - const line = loc.start.line; - btArray.unshift( - new RoAssociativeArray([ - { - name: new BrsString("filename"), - value: new BrsString(loc?.file ?? "()"), - }, - { name: new BrsString("function"), value: new BrsString(funcSignature) }, - { name: new BrsString("line_number"), value: new Int32(line) }, - ]) - ); - loc = func.callLocation; + if (!func.signature) { + continue; } + const kind = ValueKind.toString(func.signature.returns); + let args = ""; + func.signature.args.forEach((arg) => { + args += args !== "" ? "," : ""; + args += `${arg.name.text} As ${ValueKind.toString(arg.type.kind)}`; + }); + const funcSig = `${func.functionName}(${args}) As ${kind}`; + const line = loc.start.line; + const info = { filename: loc?.file ?? "()", function: funcSig, line_number: line }; + btArray.unshift(toAssociativeArray(info)); + loc = func.callLocation; } return new RoArray(btArray); } @@ -1937,6 +1931,11 @@ export class Interpreter implements Expr.Visitor, Stmt.Visitor throw err; } + /** + * Method to evaluate if a number is positive + * @param value number to evaluate + * @returns boolean indicating if the number is positive + */ private isPositive(value: number | Long): boolean { if (value instanceof Long) { return value.isPositive(); @@ -1944,6 +1943,12 @@ export class Interpreter implements Expr.Visitor, Stmt.Visitor return value >= 0; } + /** + * Method to evaluate if a number is lees than the other + * @param value Number to evaluate + * @param compare Number to compare + * @returns Boolean indicating if the number is less than the other + */ private lessThan(value: number | Long, compare: number): boolean { if (value instanceof Long) { return value.lessThan(compare); diff --git a/src/stdlib/Json.ts b/src/stdlib/Json.ts index c57e2a79..8607b4cc 100644 --- a/src/stdlib/Json.ts +++ b/src/stdlib/Json.ts @@ -3,57 +3,18 @@ import { RoArray } from "../brsTypes/components/RoArray"; import { Interpreter } from "../interpreter"; import { - BrsBoolean, BrsComponent, BrsInvalid, BrsString, BrsType, Callable, - Float, Int32, - Int64, ValueKind, StdlibArgument, isUnboxable, + brsValueOf, } from "../brsTypes"; -/** - * Converts a value to its representation as a BrsType. If no such - * representation is possible, throws an Error. - * @param {any} x Some value. - * @return {BrsType} The BrsType representation of `x`. - * @throws {Error} If `x` cannot be represented as a BrsType. - */ -function brsValueOf(x: any): BrsType { - if (x === null) { - return BrsInvalid.Instance; - } - let t: string = typeof x; - switch (t) { - case "boolean": - return BrsBoolean.from(x); - case "string": - return new BrsString(x); - case "number": - if (Number.isInteger(x)) { - return x >= -2_147_483_648 && x <= 2_147_483_647 ? new Int32(x) : new Int64(x); - } - return new Float(x); - case "object": - if (Array.isArray(x)) { - return new RoArray(x.map(brsValueOf)); - } - return new RoAssociativeArray( - Object.getOwnPropertyNames(x).map((k: string) => ({ - name: new BrsString(k), - value: brsValueOf(x[k]), - })) - ); - default: - throw new Error(`brsValueOf not implemented for: ${x} <${t}>`); - } -} - type BrsAggregate = RoAssociativeArray | RoArray; function visit(x: BrsAggregate, visited: Set): void {