Skip to content

Commit

Permalink
Implemented several improvements to SceneGraph (#87)
Browse files Browse the repository at this point in the history
* Implemented several improvements to SceneGraph

* Fixed most test cases

* Reduced unnecessary code

* Fixed typo

* Added Warning when trying to create a non-existent Node

* Fixed parser

* Fixed unit tests

* Implemented support for `infoFields`

* Prettier fix

* Simplified execute callback code and matched behavior with Roku

* Adding comment to clarify the exception
  • Loading branch information
lvcabral authored Jan 27, 2025
1 parent a1b343e commit 1c9db28
Show file tree
Hide file tree
Showing 52 changed files with 1,153 additions and 559 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ lib/
types/
node_modules/
coverage/
out/
.DS_Store
75 changes: 43 additions & 32 deletions src/LexerParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,47 @@ import pSettle from "p-settle";
const readFile = promisify(fs.readFile);

import { Lexer } from "./lexer";
import { Parser, Stmt } from "./parser";
import { Parser, Stmt } from "./parser/";
import * as PP from "./preprocessor";

import * as BrsTypes from "./brsTypes";
export { BrsTypes as types };
export { PP as preprocessor };
import { ManifestValue } from "./preprocessor/Manifest";
import * as BrsError from "./Error";
import { defaultExecutionOptions, ExecutionOptions } from "./interpreter";
import { ComponentScript } from "./scenegraph";
import { ManifestValue } from "./preprocessor/Manifest";

export function getLexerParserFn(
manifest: Map<string, ManifestValue>,
options: Partial<ExecutionOptions>
) {
const executionOptions = { ...defaultExecutionOptions, ...options };
/**
* Map file URIs to promises. The promises resolve to an array of that file's statements.
* This allows us to only parse each file once.
* Map file URIs or Source Content to promises. The promises resolve to an array of that script's statements.
* This allows us to only parse each shared file once.
*/
let memoizedStatements = new Map<string, Promise<Stmt.Statement[]>>();
return async function parse(filenames: string[]): Promise<Stmt.Statement[]> {
async function lexAndParseFile(filename: string) {
filename = filename.replace(/[\/\\]+/g, path.posix.sep);
return async function parse(scripts: ComponentScript[]): Promise<Stmt.Statement[]> {
async function lexAndParseScript(script: ComponentScript) {
let contents;
try {
contents = await readFile(filename, "utf-8");
} catch (err) {
let errno = (err as NodeJS.ErrnoException)?.errno || -4858;
return Promise.reject({
message: `brs: can't open file '${filename}': [Errno ${errno}]`,
});
let filename;
if (script.uri !== undefined) {
filename = script.uri.replace(/[\/\\]+/g, path.posix.sep);
try {
contents = await readFile(filename, "utf-8");
} catch (err) {
let errno = (err as NodeJS.ErrnoException)?.errno || -4858;
return Promise.reject({
message: `brs: can't open file '${filename}': [Errno ${errno}]`,
});
}
} else if (script.content !== undefined) {
contents = script.content;
filename = script.xmlPath || "xml";
} else {
return Promise.reject({ message: "brs: invalid script object" });
}

let lexer = new Lexer();
let preprocessor = new PP.Preprocessor();
let parser = new Parser();
Expand Down Expand Up @@ -69,34 +77,37 @@ export function getLexerParserFn(
return Promise.resolve(parseResults.statements);
}

let parsedFiles = await pSettle(
filenames.map(async (filename) => {
let maybeStatements = memoizedStatements.get(filename);
let promises: Promise<Stmt.Statement[]>[] = [];
for (let script of scripts) {
if (script.uri !== undefined) {
let maybeStatements = memoizedStatements.get(script.uri);
if (maybeStatements) {
return maybeStatements;
promises.push(maybeStatements);
} else {
let statementsPromise = lexAndParseFile(filename);
if (!memoizedStatements.has(filename)) {
memoizedStatements.set(filename, statementsPromise);
let statementsPromise = lexAndParseScript(script);
if (!memoizedStatements.has(script.uri)) {
memoizedStatements.set(script.uri, statementsPromise);
}

return statementsPromise;
promises.push(statementsPromise);
}
})
);
} else if (script.content !== undefined) {
promises.push(lexAndParseScript(script));
}
}
let parsedScripts = await pSettle(promises);

// don't execute anything if there were reading, lexing, or parsing errors
if (parsedFiles.some((file) => file.isRejected)) {
if (parsedScripts.some((script) => script.isRejected)) {
return Promise.reject({
messages: parsedFiles
.filter((file) => file.isRejected)
messages: parsedScripts
.filter((script) => script.isRejected)
.map((rejection) => rejection.reason.message),
});
}

// combine statements from all files into one array
return parsedFiles
.map((file) => file.value || [])
// combine statements from all scripts into one array
return parsedScripts
.map((script) => script.value || [])
.reduce((allStatements, fileStatements) => [...allStatements, ...fileStatements], []);
};
}
6 changes: 3 additions & 3 deletions src/brsTypes/BrsType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RoString } from "./components/RoString";
import { Int32 } from "./Int32";
import { Float } from "./Float";
import { roBoolean } from "./components/RoBoolean";
import { roInvalid } from "./components/RoInvalid";
import { RoInvalid } from "./components/RoInvalid";
import { RoAssociativeArray } from "./components/RoAssociativeArray";
import { RoArray } from "./components/RoArray";

Expand Down Expand Up @@ -150,7 +150,7 @@ export function getBrsValueFromFieldType(type: string, value?: string): BrsType
returnValue = value ? BrsBoolean.from(value === "true") : BrsBoolean.False;
break;
case "node":
returnValue = BrsInvalid.Instance;
returnValue = new RoInvalid();
break;
case "int":
case "integer":
Expand Down Expand Up @@ -394,7 +394,7 @@ export class BrsInvalid implements BrsValue, Comparable, Boxable {
}

box() {
return new roInvalid();
return new RoInvalid();
}
}

Expand Down
8 changes: 6 additions & 2 deletions src/brsTypes/components/BrsObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { RoByteArray } from "./RoByteArray";
import { RoDateTime } from "./RoDateTime";
import { RoTimespan } from "./RoTimespan";
import { createNodeByType } from "./RoSGNode";
import { roSGScreen } from "./RoSGScreen";
import { RoMessagePort } from "./RoMessagePort";
import { RoRegex } from "./RoRegex";
import { RoXMLElement } from "./RoXMLElement";
import { BrsString, BrsBoolean } from "../BrsType";
Expand All @@ -20,7 +22,7 @@ import { Float } from "../Float";
import { Int32 } from "../Int32";
import { Int64 } from "../Int64";
import { Interpreter } from "../../interpreter";
import { roInvalid } from "./RoInvalid";
import { RoInvalid } from "./RoInvalid";
import { BrsComponent } from "./BrsComponent";
import { RoAppInfo } from "./RoAppInfo";
import { RoPath } from "./RoPath";
Expand Down Expand Up @@ -98,6 +100,8 @@ export const BrsObjects = new BrsObjectsMap([
(interpreter: Interpreter, nodeType: BrsString) => createNodeByType(interpreter, nodeType),
1,
],
["roSGScreen", (interpreter: Interpreter) => new roSGScreen(interpreter)],
["roMessagePort", (_: Interpreter) => new RoMessagePort()],
[
"roRegex",
(_: Interpreter, expression: BrsString, flags: BrsString) => new RoRegex(expression, flags),
Expand All @@ -112,7 +116,7 @@ export const BrsObjects = new BrsObjectsMap([
["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],
["roInvalid", (_: Interpreter) => new RoInvalid(), -1],
]);

/**
Expand Down
27 changes: 26 additions & 1 deletion src/brsTypes/components/RoDeviceInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { BrsValue, ValueKind, BrsString, BrsBoolean, BrsInvalid } from "../BrsTy
import { BrsType, Int32, RoArray, toAssociativeArray } from "..";
import { BrsComponent } from "./BrsComponent";
import { Callable, StdlibArgument } from "../Callable";
import { RoAssociativeArray, AAMember } from "./RoAssociativeArray";
import { RoMessagePort } from "./RoMessagePort";
import { RoAssociativeArray } from "./RoAssociativeArray";
import { v4 as uuidv4 } from "uuid";

export class RoDeviceInfo extends BrsComponent implements BrsValue {
readonly kind = ValueKind.Object;

private port?: RoMessagePort;
private captionsMode = new BrsString("");
private captionsOption = new BrsString("");
private enableAppFocus = BrsBoolean.True;
Expand Down Expand Up @@ -76,6 +78,8 @@ export class RoDeviceInfo extends BrsComponent implements BrsValue {
this.getSoundEffectsVolume,
this.isAudioGuideEnabled,
this.enableAudioGuideChangedEvent,
this.setMessagePort,
this.getMessagePort,
],
});
}
Expand Down Expand Up @@ -630,4 +634,25 @@ export class RoDeviceInfo extends BrsComponent implements BrsValue {
return this.enableAudioGuideChanged;
},
});

private setMessagePort = new Callable("setMessagePort", {
signature: {
args: [new StdlibArgument("port", ValueKind.Dynamic)],
returns: ValueKind.Void,
},
impl: (_, port: RoMessagePort) => {
this.port = port;
return BrsInvalid.Instance;
},
});

private getMessagePort = new Callable("getMessagePort", {
signature: {
args: [],
returns: ValueKind.Object,
},
impl: (_) => {
return this.port ?? BrsInvalid.Instance;
},
});
}
66 changes: 66 additions & 0 deletions src/brsTypes/components/RoDeviceInfoEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { BrsValue, ValueKind, BrsBoolean } from "../BrsType";
import { BrsComponent } from "./BrsComponent";
import { BrsType, toAssociativeArray } from "..";
import { Callable } from "../Callable";
import { Interpreter } from "../../interpreter";

export class RoDeviceInfoEvent extends BrsComponent implements BrsValue {
readonly kind = ValueKind.Object;
private readonly data: any;
private readonly isStatusMsg: boolean = false;
private readonly isCaptionModeMsg: boolean = false;

constructor(data: any) {
super("roDeviceInfoEvent");
this.data = data;
if (typeof data.Mode === "string") {
this.isCaptionModeMsg = true;
} else {
this.isStatusMsg = true;
}
this.registerMethods({
ifroDeviceInfoEvent: [this.getInfo, this.isStatusMessage, this.isCaptionModeChanged],
});
}

toString(parent?: BrsType): string {
return "<Component: roDeviceInfoEvent>";
}

equalTo(other: BrsType) {
return BrsBoolean.False;
}

/** Checks if the device status has changed. */
private readonly isStatusMessage = new Callable("isStatusMessage", {
signature: {
args: [],
returns: ValueKind.Boolean,
},
impl: (_: Interpreter) => {
return BrsBoolean.from(this.isStatusMsg);
},
});

/** Indicates whether the user has changed the closed caption mode. */
private readonly isCaptionModeChanged = new Callable("isCaptionModeChanged", {
signature: {
args: [],
returns: ValueKind.Boolean,
},
impl: (_: Interpreter) => {
return BrsBoolean.from(this.isCaptionModeMsg);
},
});

/** Returns an roAssociativeArray with the current status of the device or the caption mode. */
private readonly getInfo = new Callable("getInfo", {
signature: {
args: [],
returns: ValueKind.Object,
},
impl: (_: Interpreter) => {
return toAssociativeArray(this.data);
},
});
}
4 changes: 2 additions & 2 deletions src/brsTypes/components/RoInvalid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Interpreter } from "../../interpreter";
import { BrsType } from "..";
import { Unboxable } from "../Boxing";

export class roInvalid extends BrsComponent implements BrsValue, Unboxable {
export class RoInvalid extends BrsComponent implements BrsValue, Unboxable {
readonly kind = ValueKind.Object;
private intrinsic: BrsInvalid;

Expand All @@ -31,7 +31,7 @@ export class roInvalid extends BrsComponent implements BrsValue, Unboxable {
return BrsBoolean.True;
}

if (other instanceof roInvalid) {
if (other instanceof RoInvalid) {
return BrsBoolean.True;
}

Expand Down
Loading

0 comments on commit 1c9db28

Please sign in to comment.