Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented ObjFun() and aligned behavior of CreateObject() with Roku #84

Merged
merged 4 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
2 changes: 1 addition & 1 deletion src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1195,7 +1195,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
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",
]);
});
});
18 changes: 11 additions & 7 deletions test/e2e/resources/components/roIntrinsics.brs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
sub main()
booleanObjectA = createObject("roBoolean", true)
booleanObjectB = createObject("roBoolean", false)
doubleObject = createObject("roDouble", 123.456)
floatObject = createObject("roFloat", 789.012)
integerObject = createObject("roInt", 23)
longIntegerObject = createObject("roLongInteger", 2000111222333)
booleanObjectA = createObject("roBoolean")
booleanObjectA.setBoolean(true)
booleanObjectB = createObject("roBoolean")
doubleObject = createObject("roDouble")
doubleObject.setDouble(123.456)
floatObject = createObject("roFloat")
floatObject.setFloat(789.012)
integerObject = createObject("roInt")
integerObject.setInt(23)
longIntegerObject = createObject("roLongInteger")
longIntegerObject.setLongInt(2000111222333)

print "Boolean object A " booleanObjectA.toStr()
print "Boolean object B " booleanObjectB
Expand All @@ -20,5 +25,4 @@ sub main()
print "Integer to string "integerObject.toStr()
print "LongInteger object type"type(longIntegerObject)
print "LongInteger to string "longIntegerObject.toStr()

end sub
30 changes: 30 additions & 0 deletions test/e2e/resources/stdlib/create-object.brs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
sub main()
print createObject("roBoolean")
print createObject("roBoolean", true)
print createObject("roDouble")
print createObject("roDouble", 1.0)
print createObject("roFloat")
print createObject("roFloat", 1.0)
print createObject("roInt")
print createObject("roInt", 1, 2, 3)
print createObject("roLongInteger")
print createObject("roLongInteger", 1)
print createObject("roScreen")
print createObject("roString")
print createObject("roString", "hello")
print createObject("roInvalid")
print createObject("roInvalid", 1)
print createObject("roSGNode", "Node").focusable
print createObject("roAssociativeArray").count()
try
print createObject("roAssociativeArray", {a: 1, b: 2}).count()
catch e
print e.number
end try
print createObject("roRegex")
try
print createObject("roRegex", 1)
catch e
print e.number
end try
end sub
Loading
Loading