Skip to content

Commit

Permalink
Merge branch 'master' into implement-toAssociativeArray-function
Browse files Browse the repository at this point in the history
  • Loading branch information
TwitchBronBron authored Jan 3, 2025
2 parents aa7a18c + 0afd6b6 commit f150c25
Show file tree
Hide file tree
Showing 14 changed files with 292 additions and 72 deletions.
6 changes: 4 additions & 2 deletions src/brsTypes/Callable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export class StdlibArgument implements Argument {
/** A BrightScript `function` or `sub`'s signature. */
export interface Signature {
/** The set of arguments a function accepts. */
readonly args: ReadonlyArray<Argument>;
readonly args: Argument[];
/** Whether the function accepts a variable number of arguments. */
readonly variadic?: boolean;
/** The type of BrightScript value the function will return. `sub`s must use `ValueKind.Void`. */
readonly returns: Brs.ValueKind;
}
Expand Down Expand Up @@ -280,7 +282,7 @@ export class Callable implements Brs.BrsValue {
expected: signature.args.length.toString(),
received: args.length.toString(),
});
} else if (args.length > signature.args.length) {
} else if (!signature.variadic && args.length > signature.args.length) {
reasons.push({
reason: MismatchReason.TooManyArguments,
expected: signature.args.length.toString(),
Expand Down
97 changes: 77 additions & 20 deletions src/brsTypes/components/BrsObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,37 +25,94 @@ import { BrsComponent } from "./BrsComponent";
import { RoAppInfo } from "./RoAppInfo";
import { RoPath } from "./RoPath";

// Class to define a case-insensitive map of BrightScript objects.
class BrsObjectsMap {
private readonly map = new Map<
string,
{ originalKey: string; value: Function; params: number }
>();

constructor(entries: [string, Function, number?][]) {
entries.forEach(([key, value, params]) => this.set(key, value, params));
}

get(key: string) {
const entry = this.map.get(key.toLowerCase());
return entry ? entry.value : undefined;
}

set(key: string, value: Function, params?: number) {
return this.map.set(key.toLowerCase(), {
originalKey: key,
value: value,
params: params ?? 0,
});
}

has(key: string) {
return this.map.has(key.toLowerCase());
}

delete(key: string) {
return this.map.delete(key.toLowerCase());
}

clear() {
return this.map.clear();
}

values() {
return Array.from(this.map.values()).map((entry) => entry.value);
}

keys() {
return Array.from(this.map.values()).map((entry) => entry.originalKey);
}

// Returns the number of parameters required by the object constructor.
// >=0 = exact number of parameters required
// -1 = ignore parameters, create object with no parameters
// -2 = do not check for minimum number of parameters
params(key: string) {
const entry = this.map.get(key.toLowerCase());
return entry ? entry.params : -1;
}
}

/** Map containing a list of BrightScript components that can be created. */
export const BrsObjects = new Map<string, Function>([
["roassociativearray", (_: Interpreter) => new RoAssociativeArray([])],
export const BrsObjects = new BrsObjectsMap([
["roAssociativeArray", (_: Interpreter) => new RoAssociativeArray([])],
[
"roarray",
"roArray",
(interpreter: Interpreter, capacity: Int32 | Float, resizable: BrsBoolean) =>
new RoArray(capacity, resizable),
2,
],
["rolist", (_: Interpreter) => new RoList([])],
["robytearray", (_: Interpreter) => new RoByteArray()],
["rodatetime", (_: Interpreter) => new RoDateTime()],
["rotimespan", (_: Interpreter) => new Timespan()],
["rodeviceinfo", (_: Interpreter) => new RoDeviceInfo()],
["roList", (_: Interpreter) => new RoList([])],
["roByteArray", (_: Interpreter) => new RoByteArray()],
["roDateTime", (_: Interpreter) => new RoDateTime()],
["roTimespan", (_: Interpreter) => new Timespan()],
["roDeviceInfo", (_: Interpreter) => new RoDeviceInfo()],
[
"rosgnode",
"roSGNode",
(interpreter: Interpreter, nodeType: BrsString) => createNodeByType(interpreter, nodeType),
1,
],
[
"roregex",
"roRegex",
(_: Interpreter, expression: BrsString, flags: BrsString) => new RoRegex(expression, flags),
2,
],
["roxmlelement", (_: Interpreter) => new RoXMLElement()],
["rostring", (_: Interpreter) => new RoString()],
["roboolean", (_: Interpreter, literal: BrsBoolean) => new roBoolean(literal)],
["rodouble", (_: Interpreter, literal: Double) => new roDouble(literal)],
["rofloat", (_: Interpreter, literal: Float) => new roFloat(literal)],
["roint", (_: Interpreter, literal: Int32) => new roInt(literal)],
["rolonginteger", (_: Interpreter, literal: Int64) => new roLongInteger(literal)],
["roappinfo", (_: Interpreter) => new RoAppInfo()],
["ropath", (interpreter: Interpreter, path: BrsString) => new RoPath(path)],
["roinvalid", (_: Interpreter) => new roInvalid()],
["roXMLElement", (_: Interpreter) => new RoXMLElement()],
["roString", (_: Interpreter) => new RoString(), -1],
["roBoolean", (_: Interpreter, literal: BrsBoolean) => new roBoolean(literal), -1],
["roDouble", (_: Interpreter, literal: Double) => new roDouble(literal), -1],
["roFloat", (_: Interpreter, literal: Float) => new roFloat(literal), -1],
["roInt", (_: Interpreter, literal: Int32) => new roInt(literal), -1],
["roLongInteger", (_: Interpreter, literal: Int64) => new roLongInteger(literal), -1],
["roAppInfo", (_: Interpreter) => new RoAppInfo()],
["roPath", (_: Interpreter, path: BrsString) => new RoPath(path), 1],
["roInvalid", (_: Interpreter) => new roInvalid(), -1],
]);

/**
Expand Down
6 changes: 4 additions & 2 deletions src/brsTypes/components/RoBoolean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Unboxable } from "../Boxing";

export class roBoolean extends BrsComponent implements BrsValue, Unboxable {
readonly kind = ValueKind.Object;
private intrinsic: BrsBoolean;
private intrinsic: BrsBoolean = BrsBoolean.False;

public getValue(): boolean {
return this.intrinsic.toBoolean();
Expand All @@ -16,7 +16,9 @@ export class roBoolean extends BrsComponent implements BrsValue, Unboxable {
constructor(initialValue: BrsBoolean) {
super("roBoolean");

this.intrinsic = initialValue;
if (initialValue instanceof BrsBoolean) {
this.intrinsic = initialValue;
}
this.registerMethods({
ifBoolean: [this.getBoolean, this.setBoolean],
ifToStr: [this.toStr],
Expand Down
8 changes: 4 additions & 4 deletions src/brsTypes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export * from "./coercion";
* @returns `true` if `value` is a numeric value, otherwise `false`.
*/
export function isBrsNumber(value: BrsType): value is BrsNumber {
return NumberKinds.has(value.kind);
return NumberKinds.has(value?.kind);
}

export function isNumberKind(kind: ValueKind): boolean {
Expand Down Expand Up @@ -105,7 +105,7 @@ export const PrimitiveKinds = new Set([
* @returns `true` if `value` is a string, otherwise `false`.
*/
export function isBrsString(value: BrsType): value is BrsString {
return value.kind === ValueKind.String || value instanceof RoString;
return value?.kind === ValueKind.String || value instanceof RoString;
}

/**
Expand All @@ -114,7 +114,7 @@ export function isBrsString(value: BrsType): value is BrsString {
* @returns `true` if `value` if a boolean, otherwise `false`.
*/
export function isBrsBoolean(value: BrsType): value is BrsBoolean {
return value.kind === ValueKind.Boolean;
return value?.kind === ValueKind.Boolean;
}

/**
Expand All @@ -123,7 +123,7 @@ export function isBrsBoolean(value: BrsType): value is BrsBoolean {
* @returns `true` if `value` is a Callable value, otherwise `false`.
*/
export function isBrsCallable(value: BrsType): value is Callable {
return value.kind === ValueKind.Callable;
return value?.kind === ValueKind.Callable;
}

/**
Expand Down
24 changes: 11 additions & 13 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1196,7 +1196,7 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
let signature = satisfiedSignature.signature;
args = args.map((arg, index) => {
// any arguments of type "object" must be automatically boxed
if (signature.args[index].type.kind === ValueKind.Object && isBoxable(arg)) {
if (signature.args[index]?.type.kind === ValueKind.Object && isBoxable(arg)) {
return arg.box();
}

Expand Down Expand Up @@ -1298,24 +1298,22 @@ export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType>
}

let boxedSource = isBoxable(source) ? source.box() : source;
let errorDetail = RuntimeErrorDetail.DotOnNonObject;
if (boxedSource instanceof BrsComponent) {
// This check is supposed to be placed below the try/catch block,
const invalidSource = BrsInvalid.Instance.equalTo(source).toBoolean();
// This check is supposed to be placed after method check,
// but it's here to mimic the behavior of Roku, if they fix, we move it.
if (source instanceof BrsInvalid && expression.optional) {
if (invalidSource && expression.optional) {
return source;
}
try {
const method = boxedSource.getMethod(expression.name.text);
if (method) {
return method;
}
} catch (err: any) {
this.addError(new BrsError(err.message, expression.name.location));
const method = boxedSource.getMethod(expression.name.text);
if (method) {
return method;
} else if (!invalidSource) {
errorDetail = RuntimeErrorDetail.MemberFunctionNotFound;
}
}
this.addError(
new RuntimeError(RuntimeErrorDetail.DotOnNonObject, expression.name.location)
);
this.addError(new RuntimeError(errorDetail, expression.name.location));
}

visitIndexedGet(expression: Expr.IndexedGet): BrsType {
Expand Down
34 changes: 24 additions & 10 deletions src/stdlib/CreateObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,15 @@ import {
RoAssociativeArray,
} from "../brsTypes";
import { BrsObjects } from "../brsTypes/components/BrsObjects";
import { RuntimeError, RuntimeErrorDetail } from "../Error";
import { Interpreter } from "../interpreter";
import { MockNode } from "../extensions/MockNode";

/** Creates a new instance of a given brightscript component (e.g. roAssociativeArray) */
export const CreateObject = new Callable("CreateObject", {
signature: {
args: [
new StdlibArgument("objName", ValueKind.String),
new StdlibArgument("arg1", ValueKind.Dynamic, BrsInvalid.Instance),
new StdlibArgument("arg2", ValueKind.Dynamic, BrsInvalid.Instance),
new StdlibArgument("arg3", ValueKind.Dynamic, BrsInvalid.Instance),
new StdlibArgument("arg4", ValueKind.Dynamic, BrsInvalid.Instance),
new StdlibArgument("arg5", ValueKind.Dynamic, BrsInvalid.Instance),
],
args: [new StdlibArgument("objName", ValueKind.String)],
variadic: true,
returns: ValueKind.Dynamic,
},
impl: (interpreter: Interpreter, objName: BrsString, ...additionalArgs: BrsType[]) => {
Expand All @@ -40,8 +35,27 @@ export const CreateObject = new Callable("CreateObject", {
return new MockNode(possibleMock, objToMock);
}
let ctor = BrsObjects.get(objName.value.toLowerCase());

if (ctor) {
if (ctor === undefined) {
let msg = `BRIGHTSCRIPT: ERROR: Runtime: unknown classname "${
objName.value
}": ${interpreter.formatLocation()}\n`;
interpreter.stderr.write(msg);
} else {
const minParams = BrsObjects.params(objName.value.toLowerCase());
if (minParams === -1) {
additionalArgs = [];
} else if (minParams > 0 && additionalArgs.length === 0) {
interpreter.stderr.write(
`BRIGHTSCRIPT: ERROR: Runtime: "${
objName.value
}": invalid number of parameters: ${interpreter.formatLocation()}\n`
);
return BrsInvalid.Instance;
} else if (minParams >= 0 && additionalArgs.length !== minParams) {
interpreter.addError(
new RuntimeError(RuntimeErrorDetail.RoWrongNumberOfParams, interpreter.location)
);
}
try {
return ctor(interpreter, ...additionalArgs);
} catch (err: any) {
Expand Down
33 changes: 31 additions & 2 deletions src/stdlib/GlobalUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import {
BrsString,
BrsType,
StdlibArgument,
RoAssociativeArray,
BrsInterface,
BrsComponent,
} from "../brsTypes";
import { isBoxable } from "../brsTypes/Boxing";
import { BrsComponent } from "../brsTypes/components/BrsComponent";
import { RuntimeError, RuntimeErrorDetail } from "../Error";
import { Interpreter } from "../interpreter";

let warningShown = false;
Expand Down Expand Up @@ -66,3 +66,32 @@ export const FindMemberFunction = new Callable("FindMemberFunction", {
return BrsInvalid.Instance;
},
});

export const ObjFun = new Callable("ObjFun", {
signature: {
args: [
new StdlibArgument("object", ValueKind.Object),
new StdlibArgument("iface", ValueKind.Interface),
new StdlibArgument("funName", ValueKind.String),
],
variadic: true,
returns: ValueKind.Dynamic,
},
impl: (
interpreter: Interpreter,
object: BrsComponent,
iface: BrsInterface,
funName: BrsString,
...args: BrsType[]
): BrsType => {
for (let [_, objI] of object.interfaces) {
if (iface.name === objI.name && iface.hasMethod(funName.value)) {
const func = object.getMethod(funName.value);
return func?.call(interpreter, ...args) || BrsInvalid.Instance;
}
}
interpreter.addError(
new RuntimeError(RuntimeErrorDetail.MemberFunctionNotFound, interpreter.location)
);
},
});
29 changes: 29 additions & 0 deletions test/e2e/StdLib.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,35 @@ describe("end to end standard libary", () => {
"<Interface: ifStringOps>",
"<Interface: ifIntOps>",
"<Interface: ifToStr>",
"",
"true",
]);
});

test("stdlib/create-object.brs", async () => {
await execute([resourceFile("stdlib", "create-object.brs")], outputStreams);

expect(allArgs(outputStreams.stdout.write).filter((arg) => arg !== "\n")).toEqual([
"false",
"false",
" 0",
" 0",
" 0",
" 0",
" 0",
" 0",
" 0",
" 0",
"invalid",
"",
"",
"<Component: roInvalid>",
"<Component: roInvalid>",
"false",
" 0",
" 245",
"invalid",
" 245",
]);
});
});
4 changes: 3 additions & 1 deletion test/e2e/Syntax.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,15 @@ describe("end to end syntax", () => {
await execute([resourceFile("optional-chaining-operators.brs")], outputStreams);

expect(allArgs(outputStreams.stdout.write).filter((arg) => arg !== "\n")).toEqual([
" 236",
"invalid",
"invalid",
"invalid",
"invalid",
"invalid",
"invalid",
"invalid",
"invalid as string",
" 244",
]);
});

Expand Down
Loading

0 comments on commit f150c25

Please sign in to comment.