• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brs / #142

22 Nov 2024 01:05AM UTC coverage: 89.282% (+0.1%) from 89.154%
#142

push

web-flow
Merge a7e8adaca into a699c56c7

2153 of 2605 branches covered (82.65%)

Branch coverage included in aggregate %.

24 of 24 new or added lines in 4 files covered. (100.0%)

1 existing line in 1 file now uncovered.

6027 of 6557 relevant lines covered (91.92%)

28660.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.99
/src/interpreter/index.ts
1
import { EventEmitter } from "events";
135✔
2
import * as PP from "../preprocessor";
3

4
import {
135✔
5
    BrsType,
6
    ValueKind,
7
    BrsInvalid,
8
    isBrsNumber,
9
    isBrsString,
10
    BrsBoolean,
11
    BrsString,
12
    isBrsBoolean,
13
    Int32,
14
    isBrsCallable,
15
    Uninitialized,
16
    RoArray,
17
    isIterable,
18
    Callable,
19
    BrsNumber,
20
    Float,
21
    tryCoerce,
22
    isComparable,
23
    isStringComp,
24
    isBoxedNumber,
25
    roInvalid,
26
    PrimitiveKinds,
27
    Signature,
28
    RoByteArray,
29
    RoList,
30
    RoXMLElement,
31
    RoSGNode,
32
} from "../brsTypes";
33

34
import { Lexeme, Location } from "../lexer";
135✔
35
import { isToken } from "../lexer/Token";
135✔
36
import { Expr, Stmt, ComponentScopeResolver } from "../parser";
135✔
37
import {
135✔
38
    BrsError,
39
    ErrorDetail,
40
    RuntimeError,
41
    RuntimeErrorDetail,
42
    findErrorDetail,
43
    getLoggerUsing,
44
} from "../Error";
45

46
import * as StdLib from "../stdlib";
135✔
47
import { _brs_ } from "../extensions";
135✔
48

49
import { Scope, Environment, NotFound } from "./Environment";
135✔
50
import { TypeMismatch } from "./TypeMismatch";
135✔
51
import { OutputProxy } from "./OutputProxy";
135✔
52
import { toCallable } from "./BrsFunction";
135✔
53
import { RoAssociativeArray } from "../brsTypes/components/RoAssociativeArray";
135✔
54
import MemoryFileSystem from "memory-fs";
135✔
55
import { BrsComponent } from "../brsTypes/components/BrsComponent";
135✔
56
import { isBoxable, isUnboxable } from "../brsTypes/Boxing";
135✔
57

58
import { ComponentDefinition } from "../componentprocessor";
59
import pSettle from "p-settle";
135✔
60
import { CoverageCollector } from "../coverage";
61
import { ManifestValue } from "../preprocessor/Manifest";
62
import { generateArgumentMismatchError } from "./ArgumentMismatch";
135✔
63
import Long from "long";
135✔
64

65
import chalk from "chalk";
135✔
66
import stripAnsi from "strip-ansi";
135✔
67

68
/** The set of options used to configure an interpreter's execution. */
69
export interface ExecutionOptions {
70
    /** The base path for the project. Default: process.cwd() */
71
    root: string;
72
    /** The stdout stream that brs should use. Default: process.stdout. */
73
    stdout: NodeJS.WriteStream;
74
    /** The stderr stream that brs should use. Default: process.stderr. */
75
    stderr: NodeJS.WriteStream;
76
    /** Whether or not to collect coverage statistics. Default: false. */
77
    generateCoverage: boolean;
78
    /** Additional directories to search for component definitions. Default: [] */
79
    componentDirs: string[];
80
    /** Whether or not a component library is being processed. */
81
    isComponentLibrary: boolean;
82
}
83

84
/** The default set of execution options.  Includes the `stdout`/`stderr` pair from the process that invoked `brs`. */
85
export const defaultExecutionOptions: ExecutionOptions = {
135✔
86
    root: process.cwd(),
87
    stdout: process.stdout,
88
    stderr: process.stderr,
89
    generateCoverage: false,
90
    componentDirs: [],
91
    isComponentLibrary: false,
92
};
93
Object.freeze(defaultExecutionOptions);
135✔
94

95
/** The definition of a trace point to be added to the stack trace */
96
export interface TracePoint {
97
    functionName: string;
98
    functionLocation: Location;
99
    callLocation: Location;
100
    signature?: Signature;
101
}
102

103
export class Interpreter implements Expr.Visitor<BrsType>, Stmt.Visitor<BrsType> {
135✔
104
    private _environment = new Environment();
855✔
105
    private _stack = new Array<TracePoint>();
855✔
106
    private _tryMode = false;
855✔
107
    private _coverageCollector: CoverageCollector | null = null;
855✔
108
    private _manifest: PP.Manifest | undefined;
109

110
    readonly options: ExecutionOptions;
111
    readonly stdout: OutputProxy;
112
    readonly stderr: OutputProxy;
113
    readonly temporaryVolume: MemoryFileSystem = new MemoryFileSystem();
855✔
114

115
    location: Location;
116

117
    /** Allows consumers to observe errors as they're detected. */
118
    readonly events = new EventEmitter();
855✔
119

120
    /** The set of errors detected from executing an AST. */
121
    errors: (BrsError | RuntimeError)[] = [];
855✔
122

123
    get environment() {
124
        return this._environment;
100,151✔
125
    }
126

127
    get stack() {
128
        return this._stack;
15✔
129
    }
130

131
    get manifest() {
132
        return this._manifest != null ? this._manifest : new Map<string, ManifestValue>();
12!
133
    }
134

135
    set manifest(manifest: PP.Manifest) {
136
        this._manifest = manifest;
127✔
137
    }
138

139
    addToStack(tracePoint: TracePoint) {
140
        this._stack.push(tracePoint);
22✔
141
    }
142

143
    setCoverageCollector(collector: CoverageCollector) {
144
        this._coverageCollector = collector;
1✔
145
    }
146

147
    reportCoverageHit(statement: Expr.Expression | Stmt.Statement) {
148
        if (this.options.generateCoverage && this._coverageCollector) {
151,438✔
149
            this._coverageCollector.logHit(statement);
46✔
150
        }
151
    }
152

153
    /**
154
     * Convenience function to subscribe to the `err` events emitted by `interpreter.events`.
155
     * @param errorHandler the function to call for every runtime error emitted after subscribing
156
     * @returns an object with a `dispose` function, used to unsubscribe from errors
157
     */
158
    public onError(errorHandler: (err: BrsError | RuntimeError) => void) {
159
        this.events.on("err", errorHandler);
112✔
160
        return {
112✔
161
            dispose: () => {
162
                this.events.removeListener("err", errorHandler);
×
163
            },
164
        };
165
    }
166

167
    /**
168
     * Convenience function to subscribe to a single `err` event emitted by `interpreter.events`.
169
     * @param errorHandler the function to call for the first runtime error emitted after subscribing
170
     */
171
    public onErrorOnce(errorHandler: (err: BrsError | RuntimeError) => void) {
172
        this.events.once("err", errorHandler);
×
173
    }
174

175
    /**
176
     * Builds out all the sub-environments for the given components. Components are saved into the calling interpreter
177
     * instance. This function will mutate the state of the calling interpreter.
178
     * @param componentMap Map of all components to be assigned to this interpreter
179
     * @param parseFn Function used to parse components into interpretable statements
180
     * @param options
181
     */
182
    public static async withSubEnvsFromComponents(
183
        componentMap: Map<string, ComponentDefinition>,
184
        parseFn: (filenames: string[]) => Promise<Stmt.Statement[]>,
185
        options: ExecutionOptions = defaultExecutionOptions
1✔
186
    ) {
187
        let interpreter = new Interpreter(options);
112✔
188
        interpreter.onError(getLoggerUsing(options.stderr));
112✔
189

190
        interpreter.environment.nodeDefMap = componentMap;
112✔
191

192
        let componentScopeResolver = new ComponentScopeResolver(componentMap, parseFn);
112✔
193
        await pSettle(
112✔
194
            Array.from(componentMap).map(async (componentKV) => {
195
                let [_, component] = componentKV;
1,757✔
196
                component.environment = interpreter.environment.createSubEnvironment(
1,757✔
197
                    /* includeModuleScope */ false
198
                );
199
                let statements = await componentScopeResolver.resolve(component);
1,757✔
200
                interpreter.inSubEnv((subInterpreter) => {
1,757✔
201
                    let componentMPointer = new RoAssociativeArray([]);
1,757✔
202
                    subInterpreter.environment.setM(componentMPointer);
1,757✔
203
                    subInterpreter.environment.setRootM(componentMPointer);
1,757✔
204
                    subInterpreter.exec(statements);
1,757✔
205
                    return BrsInvalid.Instance;
1,757✔
206
                }, component.environment);
207
            })
208
        );
209

210
        return interpreter;
112✔
211
    }
212

213
    /**
214
     * Merges this environment's node definition mapping with the ones included in an array of other
215
     * interpreters, acting logically equivalent to
216
     * `Object.assign(this.environment, other1.environment, other2.environment, …)`.
217
     * @param interpreters the array of interpreters who's environment's node definition maps will
218
     *                     be merged into this one
219
     */
220
    public mergeNodeDefinitionsWith(interpreters: Interpreter[]): void {
221
        interpreters.map((other) =>
111✔
222
            other.environment.nodeDefMap.forEach((value, key) =>
1✔
223
                this.environment.nodeDefMap.set(key, value)
1✔
224
            )
225
        );
226
    }
227

228
    /**
229
     * Creates a new Interpreter, including any global properties and functions.
230
     * @param options configuration for the execution, including the streams to use for `stdout` and
231
     *                `stderr` and the base directory for path resolution
232
     */
233
    constructor(options: ExecutionOptions = defaultExecutionOptions) {
597✔
234
        this.stdout = new OutputProxy(options.stdout);
855✔
235
        this.stderr = new OutputProxy(options.stderr);
855✔
236
        this.options = options;
855✔
237
        this.location = {
855✔
238
            file: "(none)",
239
            start: { line: -1, column: -1 },
240
            end: { line: -1, column: -1 },
241
        };
242

243
        Object.keys(StdLib)
855✔
244
            .map((name) => (StdLib as any)[name])
49,590✔
245
            .filter((func) => func instanceof Callable)
49,590✔
246
            .filter((func: Callable) => {
247
                if (!func.name) {
46,170!
248
                    throw new Error("Unnamed standard library function detected!");
×
249
                }
250

251
                return !!func.name;
46,170✔
252
            })
253
            .forEach((func: Callable) =>
254
                this._environment.define(Scope.Global, func.name || "", func)
46,170!
255
            );
256

257
        this._environment.define(Scope.Global, "_brs_", _brs_);
855✔
258
    }
259

260
    /**
261
     * Temporarily sets an interpreter's environment to the provided one, then
262
     * passes the sub-interpreter to the provided JavaScript function. Always
263
     * reverts the current interpreter's environment to its original value.
264
     * @param func the JavaScript function to execute with the sub interpreter.
265
     * @param environment (Optional) the environment to run the interpreter in.
266
     */
267
    inSubEnv(func: (interpreter: Interpreter) => BrsType, environment?: Environment): BrsType {
268
        let originalEnvironment = this._environment;
23,644✔
269
        let newEnv = environment ?? this._environment.createSubEnvironment();
23,644✔
270
        // Set the focused node of the sub env, because our current env has the most up-to-date reference.
271
        newEnv.setFocusedNode(this._environment.getFocusedNode());
23,644✔
272

273
        try {
23,644✔
274
            this._environment = newEnv;
23,644✔
275
            const returnValue = func(this);
23,644✔
276
            this._environment = originalEnvironment;
23,353✔
277
            this._environment.setFocusedNode(newEnv.getFocusedNode());
23,353✔
278
            return returnValue;
23,353✔
279
        } catch (err) {
280
            this._environment = originalEnvironment;
291✔
281
            this._environment.setFocusedNode(newEnv.getFocusedNode());
291✔
282
            throw err;
291✔
283
        }
284
    }
285

286
    exec(statements: ReadonlyArray<Stmt.Statement>, ...args: BrsType[]) {
287
        let results = statements.map((statement) => this.execute(statement));
5,060✔
288
        try {
2,032✔
289
            let mainVariable = new Expr.Variable({
2,032✔
290
                kind: Lexeme.Identifier,
291
                text: "main",
292
                isReserved: false,
293
                location: {
294
                    start: {
295
                        line: -1,
296
                        column: -1,
297
                    },
298
                    end: {
299
                        line: -1,
300
                        column: -1,
301
                    },
302
                    file: "(internal)",
303
                },
304
            });
305

306
            let maybeMain = this.evaluate(mainVariable);
2,032✔
307

308
            if (maybeMain.kind === ValueKind.Callable) {
2,032✔
309
                results = [
104✔
310
                    this.evaluate(
311
                        new Expr.Call(
312
                            mainVariable,
313
                            mainVariable.name,
314
                            args.map((arg) => new Expr.Literal(arg, mainVariable.location))
8✔
315
                        )
316
                    ),
317
                ];
318
            }
319
        } catch (err) {
320
            if (err instanceof Stmt.ReturnValue) {
12!
321
                results = [err.value || BrsInvalid.Instance];
×
322
            } else if (!(err instanceof BrsError)) {
12!
323
                // Swallow BrsErrors, because they should have been exposed to the user downstream.
324
                throw err;
×
325
            }
326
        }
327

328
        return results;
2,032✔
329
    }
330

331
    getCallableFunction(functionName: string): Callable | undefined {
332
        let callbackVariable = new Expr.Variable({
63✔
333
            kind: Lexeme.Identifier,
334
            text: functionName,
335
            isReserved: false,
336
            location: {
337
                start: {
338
                    line: -1,
339
                    column: -1,
340
                },
341
                end: {
342
                    line: -1,
343
                    column: -1,
344
                },
345
                file: "(internal)",
346
            },
347
        });
348

349
        let maybeCallback = this.evaluate(callbackVariable);
63✔
350
        if (maybeCallback.kind === ValueKind.Callable) {
63✔
351
            return maybeCallback;
61✔
352
        }
353

354
        // If we can't find the function, return undefined and let the consumer handle it.
355
        return;
2✔
356
    }
357

358
    /**
359
     * Returns the init method (if any) in the current environment as a Callable
360
     */
361
    getInitMethod(): BrsType {
362
        let initVariable = new Expr.Variable({
81✔
363
            kind: Lexeme.Identifier,
364
            text: "init",
365
            isReserved: false,
366
            location: {
367
                start: {
368
                    line: -1,
369
                    column: -1,
370
                },
371
                end: {
372
                    line: -1,
373
                    column: -1,
374
                },
375
                file: "(internal)",
376
            },
377
        });
378

379
        return this.evaluate(initVariable);
81✔
380
    }
381

382
    visitLibrary(statement: Stmt.Library): BrsInvalid {
383
        this.stderr.write("WARNING: 'Library' statement implemented as no-op");
×
384
        return BrsInvalid.Instance;
×
385
    }
386

387
    visitNamedFunction(statement: Stmt.Function): BrsType {
388
        if (statement.name.isReserved) {
4,491✔
389
            this.addError(
1✔
390
                new BrsError(
391
                    `Cannot create a named function with reserved name '${statement.name.text}'`,
392
                    statement.name.location
393
                )
394
            );
395
        }
396

397
        if (this.environment.has(statement.name, [Scope.Module])) {
4,490!
398
            // TODO: Figure out how to determine where the original version was declared
399
            // Maybe `Environment.define` records the location along with the value?
400
            this.addError(
×
401
                new BrsError(
402
                    `Attempting to declare function '${statement.name.text}', but ` +
403
                        `a property of that name already exists in this scope.`,
404
                    statement.name.location
405
                )
406
            );
407
        }
408

409
        this.environment.define(
4,490✔
410
            Scope.Module,
411
            statement.name.text!,
412
            toCallable(statement.func, statement.name.text)
413
        );
414
        return BrsInvalid.Instance;
4,490✔
415
    }
416

417
    visitReturn(statement: Stmt.Return): never {
418
        if (!statement.value) {
129✔
419
            throw new Stmt.ReturnValue(statement.tokens.return.location);
2✔
420
        }
421

422
        let toReturn = this.evaluate(statement.value);
127✔
423
        throw new Stmt.ReturnValue(statement.tokens.return.location, toReturn);
127✔
424
    }
425

426
    visitExpression(statement: Stmt.Expression): BrsType {
427
        return this.evaluate(statement.expression);
4,614✔
428
    }
429

430
    visitPrint(statement: Stmt.Print): BrsType {
431
        // the `tab` function is only in-scope while executing print statements
432
        this.environment.define(Scope.Function, "Tab", StdLib.Tab);
1,211✔
433

434
        statement.expressions.forEach((printable, index) => {
1,211✔
435
            if (isToken(printable)) {
1,752✔
436
                switch (printable.kind) {
35!
437
                    case Lexeme.Comma:
438
                        this.stdout.write(" ".repeat(16 - (this.stdout.position() % 16)));
5✔
439
                        break;
5✔
440
                    case Lexeme.Semicolon:
441
                        break;
30✔
442
                    default:
443
                        this.addError(
×
444
                            new BrsError(
445
                                `Found unexpected print separator '${printable.text}'`,
446
                                printable.location
447
                            )
448
                        );
449
                }
450
            } else {
451
                const toPrint = this.evaluate(printable);
1,717✔
452
                const isNumber = isBrsNumber(toPrint) || isBoxedNumber(toPrint);
1,712✔
453
                const str = isNumber && this.isPositive(toPrint.getValue()) ? " " : "";
1,712✔
454
                this.stdout.write(colorize(str + toPrint.toString()));
1,712✔
455
            }
456
        });
457

458
        let lastExpression = statement.expressions[statement.expressions.length - 1];
1,206✔
459
        if (!isToken(lastExpression) || lastExpression.kind !== Lexeme.Semicolon) {
1,206✔
460
            this.stdout.write("\n");
1,204✔
461
        }
462

463
        // `tab` is only in-scope when executing print statements, so remove it before we leave
464
        this.environment.remove("Tab");
1,206✔
465

466
        return BrsInvalid.Instance;
1,206✔
467
    }
468

469
    visitAssignment(statement: Stmt.Assignment): BrsType {
470
        if (statement.name.isReserved) {
8,949✔
471
            this.addError(
1✔
472
                new BrsError(
473
                    `Cannot assign a value to reserved name '${statement.name.text}'`,
474
                    statement.name.location
475
                )
476
            );
477
            return BrsInvalid.Instance;
×
478
        }
479

480
        let value = this.evaluate(statement.value);
8,948✔
481

482
        let name = statement.name.text;
8,946✔
483

484
        const typeDesignators: Record<string, ValueKind> = {
8,946✔
485
            $: ValueKind.String,
486
            "%": ValueKind.Int32,
487
            "!": ValueKind.Float,
488
            "#": ValueKind.Double,
489
            "&": ValueKind.Int64,
490
        };
491
        let requiredType = typeDesignators[name.charAt(name.length - 1)];
8,946✔
492

493
        if (requiredType) {
8,946✔
494
            let coercedValue = tryCoerce(value, requiredType);
49✔
495
            if (coercedValue != null) {
49✔
496
                value = coercedValue;
40✔
497
            } else {
498
                this.addError(
9✔
499
                    new TypeMismatch({
500
                        message: `Type Mismatch. Attempting to assign incorrect value to statically-typed variable '${name}'`,
501
                        left: {
502
                            type: requiredType,
503
                            location: statement.name.location,
504
                        },
505
                        right: {
506
                            type: value,
507
                            location: statement.value.location,
508
                        },
509
                    })
510
                );
511
            }
512
        }
513

514
        this.environment.define(Scope.Function, statement.name.text, value);
8,937✔
515
        return BrsInvalid.Instance;
8,937✔
516
    }
517

518
    visitDim(statement: Stmt.Dim): BrsType {
519
        if (statement.name.isReserved) {
3!
520
            this.addError(
×
521
                new BrsError(
522
                    `Cannot assign a value to reserved name '${statement.name.text}'`,
523
                    statement.name.location
524
                )
525
            );
526
            return BrsInvalid.Instance;
×
527
        }
528

529
        let dimensionValues: number[] = [];
3✔
530
        statement.dimensions.forEach((expr) => {
3✔
531
            let val = this.evaluate(expr);
5✔
532
            if (val.kind !== ValueKind.Int32) {
5!
533
                this.addError(
×
534
                    new RuntimeError(RuntimeErrorDetail.NonNumericArrayIndex, expr.location)
535
                );
536
            }
537
            // dim takes max-index, so +1 to get the actual array size
538
            dimensionValues.push(val.getValue() + 1);
5✔
539
            return;
5✔
540
        });
541

542
        let createArrayTree = (dimIndex: number = 0): RoArray => {
3✔
543
            let children: RoArray[] = [];
68✔
544
            let size = dimensionValues[dimIndex];
68✔
545
            for (let i = 0; i < size; i++) {
68✔
546
                if (dimIndex < dimensionValues.length) {
65✔
547
                    let subchildren = createArrayTree(dimIndex + 1);
65✔
548
                    if (subchildren !== undefined) children.push(subchildren);
65✔
549
                }
550
            }
551
            let child = new RoArray(children);
68✔
552

553
            return child;
68✔
554
        };
555

556
        let array = createArrayTree();
3✔
557

558
        this.environment.define(Scope.Function, statement.name.text, array);
3✔
559

560
        return BrsInvalid.Instance;
3✔
561
    }
562

563
    visitBinary(expression: Expr.Binary) {
564
        let lexeme = expression.token.kind;
18,124✔
565
        let left = this.evaluate(expression.left);
18,124✔
566
        let right: BrsType = BrsInvalid.Instance;
18,124✔
567

568
        if (lexeme !== Lexeme.And && lexeme !== Lexeme.Or) {
18,124✔
569
            // don't evaluate right-hand-side of boolean expressions, to preserve short-circuiting
570
            // behavior found in other languages. e.g. `foo() && bar()` won't execute `bar()` if
571
            // `foo()` returns `false`.
572
            right = this.evaluate(expression.right);
18,091✔
573
        }
574

575
        // Unbox Numeric or Invalid components to intrinsic types
576
        if (isBoxedNumber(left) || left instanceof roInvalid) {
18,122✔
577
            left = left.unbox();
8✔
578
        }
579
        if (isBoxedNumber(right) || right instanceof roInvalid) {
18,122✔
580
            right = right.unbox();
5✔
581
        }
582

583
        /**
584
         * Determines whether or not the provided pair of values are allowed to be compared to each other.
585
         * @param left the left-hand side of a comparison operator
586
         * @param operator the operator to use when comparing `left` and `right`
587
         * @param right the right-hand side of a comparison operator
588
         * @returns `true` if `left` and `right` are allowed to be compared to each other with `operator`,
589
         *          otherwise `false`.
590
         */
591
        function canCheckEquality(left: BrsType, operator: Lexeme, right: BrsType): boolean {
592
            if (left.kind === ValueKind.Invalid || right.kind === ValueKind.Invalid) {
8,284✔
593
                // anything can be checked for *equality* with `invalid`, but greater than / less than comparisons
594
                // are type mismatches
595
                return operator === Lexeme.Equal || operator === Lexeme.LessGreater;
67✔
596
            }
597

598
            return (
8,217✔
599
                (left.kind < ValueKind.Dynamic || isUnboxable(left) || isComparable(left)) &&
16,439!
600
                (right.kind < ValueKind.Dynamic || isUnboxable(right) || isComparable(right))
601
            );
602
        }
603

604
        switch (lexeme) {
18,122!
605
            case Lexeme.LeftShift:
606
            case Lexeme.LeftShiftEqual:
607
                if (
11✔
608
                    isBrsNumber(left) &&
43✔
609
                    isBrsNumber(right) &&
610
                    this.isPositive(right.getValue()) &&
611
                    this.lessThan(right.getValue(), 32)
612
                ) {
613
                    return left.leftShift(right);
7✔
614
                } else if (isBrsNumber(left) && isBrsNumber(right)) {
4!
615
                    return this.addError(
4✔
616
                        new RuntimeError(RuntimeErrorDetail.BadBitShift, expression.right.location)
617
                    );
618
                } else {
619
                    return this.addError(
×
620
                        new TypeMismatch({
621
                            message: `Operator "<<" can't be applied to`,
622
                            left: {
623
                                type: left,
624
                                location: expression.left.location,
625
                            },
626
                            right: {
627
                                type: right,
628
                                location: expression.right.location,
629
                            },
630
                        })
631
                    );
632
                }
633
            case Lexeme.RightShift:
634
            case Lexeme.RightShiftEqual:
635
                if (
9✔
636
                    isBrsNumber(left) &&
35✔
637
                    isBrsNumber(right) &&
638
                    this.isPositive(right.getValue()) &&
639
                    this.lessThan(right.getValue(), 32)
640
                ) {
641
                    return left.rightShift(right);
7✔
642
                } else if (isBrsNumber(left) && isBrsNumber(right)) {
2!
643
                    return this.addError(
2✔
644
                        new RuntimeError(RuntimeErrorDetail.BadBitShift, expression.right.location)
645
                    );
646
                } else {
647
                    return this.addError(
×
648
                        new TypeMismatch({
649
                            message: `Operator ">>" can't be applied to`,
650
                            left: {
651
                                type: left,
652
                                location: expression.left.location,
653
                            },
654
                            right: {
655
                                type: right,
656
                                location: expression.right.location,
657
                            },
658
                        })
659
                    );
660
                }
661
            case Lexeme.Minus:
662
            case Lexeme.MinusEqual:
663
                if (isBrsNumber(left) && isBrsNumber(right)) {
138!
664
                    return left.subtract(right);
138✔
665
                } else {
666
                    return this.addError(
×
667
                        new TypeMismatch({
668
                            message: `Operator "-" can't be applied to`,
669
                            left: {
670
                                type: left,
671
                                location: expression.left.location,
672
                            },
673
                            right: {
674
                                type: right,
675
                                location: expression.right.location,
676
                            },
677
                        })
678
                    );
679
                }
680
            case Lexeme.Star:
681
            case Lexeme.StarEqual:
682
                if (isBrsNumber(left) && isBrsNumber(right)) {
19✔
683
                    return left.multiply(right);
18✔
684
                } else {
685
                    return this.addError(
1✔
686
                        new TypeMismatch({
687
                            message: `Operator "*" can't be applied to`,
688
                            left: {
689
                                type: left,
690
                                location: expression.left.location,
691
                            },
692
                            right: {
693
                                type: right,
694
                                location: expression.right.location,
695
                            },
696
                        })
697
                    );
698
                }
699
            case Lexeme.Caret:
700
                if (isBrsNumber(left) && isBrsNumber(right)) {
7!
701
                    return left.pow(right);
7✔
702
                } else {
703
                    return this.addError(
×
704
                        new TypeMismatch({
705
                            message: `Operator "^" can't be applied to`,
706
                            left: {
707
                                type: left,
708
                                location: expression.left.location,
709
                            },
710
                            right: {
711
                                type: right,
712
                                location: expression.right.location,
713
                            },
714
                        })
715
                    );
716
                }
717
            case Lexeme.Slash:
718
            case Lexeme.SlashEqual:
719
                if (isBrsNumber(left) && isBrsNumber(right)) {
11✔
720
                    return left.divide(right);
11✔
721
                }
722
                return this.addError(
×
723
                    new TypeMismatch({
724
                        message: `Operator "/" can't be applied to`,
725
                        left: {
726
                            type: left,
727
                            location: expression.left.location,
728
                        },
729
                        right: {
730
                            type: right,
731
                            location: expression.right.location,
732
                        },
733
                    })
734
                );
735
            case Lexeme.Mod:
736
                if (isBrsNumber(left) && isBrsNumber(right)) {
9!
737
                    return left.modulo(right);
9✔
738
                } else {
739
                    return this.addError(
×
740
                        new TypeMismatch({
741
                            message: `Operator "mod" can't be applied to`,
742
                            left: {
743
                                type: left,
744
                                location: expression.left.location,
745
                            },
746
                            right: {
747
                                type: right,
748
                                location: expression.right.location,
749
                            },
750
                        })
751
                    );
752
                }
753
            case Lexeme.Backslash:
754
            case Lexeme.BackslashEqual:
755
                if (isBrsNumber(left) && isBrsNumber(right)) {
3!
756
                    return left.intDivide(right);
3✔
757
                } else {
758
                    return this.addError(
×
759
                        new TypeMismatch({
760
                            message: `Operator "\\" can't be applied to`,
761
                            left: {
762
                                type: left,
763
                                location: expression.left.location,
764
                            },
765
                            right: {
766
                                type: right,
767
                                location: expression.right.location,
768
                            },
769
                        })
770
                    );
771
                }
772
            case Lexeme.Plus:
773
            case Lexeme.PlusEqual:
774
                if (isBrsNumber(left) && isBrsNumber(right)) {
9,162✔
775
                    return left.add(right);
8,407✔
776
                } else if (isStringComp(left) && isStringComp(right)) {
755✔
777
                    return left.concat(right);
754✔
778
                } else {
779
                    return this.addError(
1✔
780
                        new TypeMismatch({
781
                            message: `Operator "+" can't be applied to`,
782
                            left: {
783
                                type: left,
784
                                location: expression.left.location,
785
                            },
786
                            right: {
787
                                type: right,
788
                                location: expression.right.location,
789
                            },
790
                        })
791
                    );
792
                }
793
            case Lexeme.Greater:
794
                if (
189✔
795
                    (isBrsNumber(left) && isBrsNumber(right)) ||
388✔
796
                    (isStringComp(left) && isStringComp(right))
797
                ) {
798
                    return left.greaterThan(right);
172✔
799
                }
800

801
                return this.addError(
17✔
802
                    new TypeMismatch({
803
                        message: `Operator ">" can't be applied to`,
804
                        left: {
805
                            type: left,
806
                            location: expression.left.location,
807
                        },
808
                        right: {
809
                            type: right,
810
                            location: expression.right.location,
811
                        },
812
                    })
813
                );
814

815
            case Lexeme.GreaterEqual:
816
                if (
35✔
817
                    (isBrsNumber(left) && isBrsNumber(right)) ||
80✔
818
                    (isStringComp(left) && isStringComp(right))
819
                ) {
820
                    return left.greaterThan(right).or(left.equalTo(right));
18✔
821
                }
822

823
                return this.addError(
17✔
824
                    new TypeMismatch({
825
                        message: `Operator ">=" can't be applied to`,
826
                        left: {
827
                            type: left,
828
                            location: expression.left.location,
829
                        },
830
                        right: {
831
                            type: right,
832
                            location: expression.right.location,
833
                        },
834
                    })
835
                );
836

837
            case Lexeme.Less:
838
                if (
178✔
839
                    (isBrsNumber(left) && isBrsNumber(right)) ||
369✔
840
                    (isStringComp(left) && isStringComp(right))
841
                ) {
842
                    return left.lessThan(right);
158✔
843
                }
844

845
                return this.addError(
20✔
846
                    new TypeMismatch({
847
                        message: `Operator "<" can't be applied to`,
848
                        left: {
849
                            type: left,
850
                            location: expression.left.location,
851
                        },
852
                        right: {
853
                            type: right,
854
                            location: expression.right.location,
855
                        },
856
                    })
857
                );
858
            case Lexeme.LessEqual:
859
                if (
34✔
860
                    (isBrsNumber(left) && isBrsNumber(right)) ||
78✔
861
                    (isStringComp(left) && isStringComp(right))
862
                ) {
863
                    return left.lessThan(right).or(left.equalTo(right));
17✔
864
                }
865

866
                return this.addError(
17✔
867
                    new TypeMismatch({
868
                        message: `Operator "<=" can't be applied to`,
869
                        left: {
870
                            type: left,
871
                            location: expression.left.location,
872
                        },
873
                        right: {
874
                            type: right,
875
                            location: expression.right.location,
876
                        },
877
                    })
878
                );
879
            case Lexeme.Equal:
880
                if (canCheckEquality(left, lexeme, right)) {
121✔
881
                    return left.equalTo(right);
120✔
882
                }
883

884
                return this.addError(
1✔
885
                    new TypeMismatch({
886
                        message: `Operator "=" can't be applied to`,
887
                        left: {
888
                            type: left,
889
                            location: expression.left.location,
890
                        },
891
                        right: {
892
                            type: right,
893
                            location: expression.right.location,
894
                        },
895
                    })
896
                );
897
            case Lexeme.LessGreater:
898
                if (canCheckEquality(left, lexeme, right)) {
8,163✔
899
                    return left.equalTo(right).not();
8,162✔
900
                }
901

902
                return this.addError(
1✔
903
                    new TypeMismatch({
904
                        message: `Operator "<>" can't be applied to`,
905
                        left: {
906
                            type: left,
907
                            location: expression.left.location,
908
                        },
909
                        right: {
910
                            type: right,
911
                            location: expression.right.location,
912
                        },
913
                    })
914
                );
915
            case Lexeme.And:
916
                if (isBrsBoolean(left) && !left.toBoolean()) {
26✔
917
                    // short-circuit ANDs - don't evaluate RHS if LHS is false
918
                    return BrsBoolean.False;
12✔
919
                } else if (isBrsBoolean(left)) {
14✔
920
                    right = this.evaluate(expression.right);
13✔
921
                    if (isBrsBoolean(right) || isBrsNumber(right)) {
13✔
922
                        return (left as BrsBoolean).and(right);
12✔
923
                    }
924

925
                    return this.addError(
1✔
926
                        new TypeMismatch({
927
                            message: `Operator "and" can't be applied to`,
928
                            left: {
929
                                type: left,
930
                                location: expression.left.location,
931
                            },
932
                            right: {
933
                                type: right,
934
                                location: expression.right.location,
935
                            },
936
                        })
937
                    );
938
                } else if (isBrsNumber(left)) {
1!
939
                    right = this.evaluate(expression.right);
1✔
940

941
                    if (isBrsNumber(right) || isBrsBoolean(right)) {
1!
942
                        return left.and(right);
1✔
943
                    }
944

945
                    return this.addError(
×
946
                        new TypeMismatch({
947
                            message: `Operator "and" can't be applied to`,
948
                            left: {
949
                                type: left,
950
                                location: expression.left.location,
951
                            },
952
                            right: {
953
                                type: right,
954
                                location: expression.right.location,
955
                            },
956
                        })
957
                    );
958
                } else {
959
                    return this.addError(
×
960
                        new TypeMismatch({
961
                            message: `Operator "and" can't be applied to`,
962
                            left: {
963
                                type: left,
964
                                location: expression.left.location,
965
                            },
966
                            right: {
967
                                type: right,
968
                                location: expression.right.location,
969
                            },
970
                        })
971
                    );
972
                }
973
            case Lexeme.Or:
974
                if (isBrsBoolean(left) && left.toBoolean()) {
7✔
975
                    // short-circuit ORs - don't evaluate RHS if LHS is true
976
                    return BrsBoolean.True;
2✔
977
                } else if (isBrsBoolean(left)) {
5✔
978
                    right = this.evaluate(expression.right);
4✔
979
                    if (isBrsBoolean(right) || isBrsNumber(right)) {
4✔
980
                        return (left as BrsBoolean).or(right);
3✔
981
                    } else {
982
                        return this.addError(
1✔
983
                            new TypeMismatch({
984
                                message: `Operator "or" can't be applied to`,
985
                                left: {
986
                                    type: left,
987
                                    location: expression.left.location,
988
                                },
989
                                right: {
990
                                    type: right,
991
                                    location: expression.right.location,
992
                                },
993
                            })
994
                        );
995
                    }
996
                } else if (isBrsNumber(left)) {
1!
997
                    right = this.evaluate(expression.right);
1✔
998
                    if (isBrsNumber(right) || isBrsBoolean(right)) {
1!
999
                        return left.or(right);
1✔
1000
                    }
1001

1002
                    return this.addError(
×
1003
                        new TypeMismatch({
1004
                            message: `Operator "or" can't be applied to`,
1005
                            left: {
1006
                                type: left,
1007
                                location: expression.left.location,
1008
                            },
1009
                            right: {
1010
                                type: right,
1011
                                location: expression.right.location,
1012
                            },
1013
                        })
1014
                    );
1015
                } else {
1016
                    return this.addError(
×
1017
                        new TypeMismatch({
1018
                            message: `Operator "or" can't be applied to`,
1019
                            left: {
1020
                                type: left,
1021
                                location: expression.left.location,
1022
                            },
1023
                            right: {
1024
                                type: right,
1025
                                location: expression.right.location,
1026
                            },
1027
                        })
1028
                    );
1029
                }
1030
            default:
1031
                this.addError(
×
1032
                    new BrsError(
1033
                        `Received unexpected token kind '${expression.token.kind}'`,
1034
                        expression.token.location
1035
                    )
1036
                );
1037
        }
1038
    }
1039

1040
    visitTryCatch(statement: Stmt.TryCatch): BrsInvalid {
1041
        let tryMode = this._tryMode;
5✔
1042
        try {
5✔
1043
            this._tryMode = true;
5✔
1044
            this.visitBlock(statement.tryBlock);
5✔
1045
            this._tryMode = tryMode;
1✔
1046
        } catch (err: any) {
1047
            this._tryMode = tryMode;
4✔
1048
            if (!(err instanceof BrsError)) {
4!
1049
                throw err;
×
1050
            }
1051
            const btArray = this.formatBacktrace(err.location, err.backTrace);
4✔
1052
            let errDetail = RuntimeErrorDetail.Internal;
4✔
1053
            let errMessage = err.message;
4✔
1054
            if (err instanceof RuntimeError) {
4✔
1055
                errDetail = err.errorDetail;
4✔
1056
            }
1057
            const errorAA = new RoAssociativeArray([
4✔
1058
                { name: new BrsString("backtrace"), value: btArray },
1059
                { name: new BrsString("message"), value: new BrsString(errMessage) },
1060
                { name: new BrsString("number"), value: new Int32(errDetail.errno) },
1061
                { name: new BrsString("rethrown"), value: BrsBoolean.False },
1062
            ]);
1063
            if (err instanceof RuntimeError && err.extraFields?.size) {
4✔
1064
                for (const [key, value] of err.extraFields) {
2✔
1065
                    errorAA.set(new BrsString(key), value);
4✔
1066
                    if (key === "rethrown" && toBool(value)) {
4✔
1067
                        errorAA.set(new BrsString("rethrow_backtrace"), btArray);
1✔
1068
                    }
1069
                }
1070
            }
1071
            this.environment.define(Scope.Function, statement.errorBinding.name.text, errorAA);
4✔
1072
            this.visitBlock(statement.catchBlock);
4✔
1073
        }
1074
        return BrsInvalid.Instance;
4✔
1075
        // Helper Function
1076
        function toBool(value: BrsType): boolean {
1077
            return isBrsBoolean(value) && value.toBoolean();
1✔
1078
        }
1079
    }
1080

1081
    visitThrow(statement: Stmt.Throw): never {
1082
        let errDetail = RuntimeErrorDetail.UserDefined;
2✔
1083
        errDetail.message = "";
2✔
1084
        const extraFields: Map<string, BrsType> = new Map<string, BrsType>();
2✔
1085
        let toThrow = this.evaluate(statement.value);
2✔
1086
        if (isStringComp(toThrow)) {
2!
1087
            errDetail.message = toThrow.getValue();
×
1088
        } else if (toThrow instanceof RoAssociativeArray) {
2!
1089
            for (const [key, element] of toThrow.elements) {
2✔
1090
                if (key.toLowerCase() === "number") {
8✔
1091
                    errDetail = validateErrorNumber(element, errDetail);
2✔
1092
                } else if (key.toLowerCase() === "message") {
6✔
1093
                    errDetail = validateErrorMessage(element, errDetail);
2✔
1094
                } else if (key.toLowerCase() === "backtrace") {
4✔
1095
                    if (element instanceof RoArray) {
1!
1096
                        extraFields.set("backtrace", element);
1✔
1097
                        extraFields.set("rethrown", BrsBoolean.True);
1✔
1098
                    } else {
1099
                        errDetail = RuntimeErrorDetail.MalformedThrow;
×
1100
                        errDetail.message = `Thrown "backtrace" is not an object.`;
×
1101
                    }
1102
                } else if (key.toLowerCase() !== "rethrown") {
3✔
1103
                    extraFields.set(key, element);
2✔
1104
                }
1105
                if (errDetail.errno === RuntimeErrorDetail.MalformedThrow.errno) {
8!
1106
                    extraFields.clear();
×
1107
                    break;
×
1108
                }
1109
            }
1110
        } else {
1111
            errDetail = RuntimeErrorDetail.MalformedThrow;
×
1112
            errDetail.message = `Thrown value neither string nor roAssociativeArray.`;
×
1113
        }
1114
        throw new RuntimeError(errDetail, statement.location, this._stack.slice(), extraFields);
2✔
1115
        // Validation Functions
1116
        function validateErrorNumber(element: BrsType, errDetail: ErrorDetail): ErrorDetail {
1117
            if (element instanceof Int32) {
2!
1118
                errDetail.errno = element.getValue();
2✔
1119
                if (errDetail.message === "") {
2!
1120
                    const foundErr = findErrorDetail(element.getValue());
×
1121
                    errDetail.message = foundErr ? foundErr.message : "UNKNOWN ERROR";
×
1122
                }
1123
            } else if (!(element instanceof BrsInvalid)) {
×
1124
                return {
×
1125
                    errno: RuntimeErrorDetail.MalformedThrow.errno,
1126
                    message: `Thrown "number" is not an integer.`,
1127
                };
1128
            }
1129
            return errDetail;
2✔
1130
        }
1131
        function validateErrorMessage(element: BrsType, errDetail: ErrorDetail): ErrorDetail {
1132
            if (element instanceof BrsString) {
2!
1133
                errDetail.message = element.toString();
2✔
1134
            } else if (!(element instanceof BrsInvalid)) {
×
1135
                return {
×
1136
                    errno: RuntimeErrorDetail.MalformedThrow.errno,
1137
                    message: `Thrown "message" is not a string.`,
1138
                };
1139
            }
1140
            return errDetail;
2✔
1141
        }
1142
    }
1143

1144
    visitBlock(block: Stmt.Block): BrsType {
1145
        block.statements.forEach((statement) => this.execute(statement));
14,863✔
1146
        return BrsInvalid.Instance;
8,873✔
1147
    }
1148

1149
    visitContinueFor(statement: Stmt.ContinueFor): never {
1150
        throw new Stmt.ContinueForReason(statement.location);
10✔
1151
    }
1152

1153
    visitExitFor(statement: Stmt.ExitFor): never {
1154
        throw new Stmt.ExitForReason(statement.location);
3✔
1155
    }
1156

1157
    visitContinueWhile(statement: Stmt.ContinueWhile): never {
1158
        throw new Stmt.ContinueWhileReason(statement.location);
6✔
1159
    }
1160

1161
    visitExitWhile(statement: Stmt.ExitWhile): never {
1162
        throw new Stmt.ExitWhileReason(statement.location);
2✔
1163
    }
1164

1165
    visitCall(expression: Expr.Call) {
1166
        let functionName = "[anonymous function]";
9,879✔
1167
        // TODO: auto-box
1168
        if (
9,879✔
1169
            expression.callee instanceof Expr.Variable ||
19,144✔
1170
            expression.callee instanceof Expr.DottedGet
1171
        ) {
1172
            functionName = expression.callee.name.text;
9,876✔
1173
        }
1174

1175
        // evaluate the function to call (it could be the result of another function call)
1176
        const callee = this.evaluate(expression.callee);
9,879✔
1177
        // evaluate all of the arguments as well (they could also be function calls)
1178
        let args = expression.args.map(this.evaluate, this);
9,879✔
1179

1180
        if (!isBrsCallable(callee)) {
9,879✔
1181
            if (callee instanceof BrsInvalid && expression.optional) {
9✔
1182
                return callee;
4✔
1183
            }
1184
            this.addError(
5✔
1185
                new RuntimeError(RuntimeErrorDetail.NotAFunction, expression.closingParen.location)
1186
            );
1187
        }
1188

1189
        functionName = callee.getName();
9,870✔
1190

1191
        let satisfiedSignature = callee.getFirstSatisfiedSignature(args);
9,870✔
1192

1193
        if (satisfiedSignature) {
9,870✔
1194
            try {
9,863✔
1195
                let signature = satisfiedSignature.signature;
9,863✔
1196
                args = args.map((arg, index) => {
9,863✔
1197
                    // any arguments of type "object" must be automatically boxed
1198
                    if (signature.args[index].type.kind === ValueKind.Object && isBoxable(arg)) {
5,130✔
1199
                        return arg.box();
6✔
1200
                    }
1201

1202
                    return arg;
5,124✔
1203
                });
1204
                let mPointer = this._environment.getRootM();
9,863✔
1205
                if (expression.callee instanceof Expr.DottedGet) {
9,863✔
1206
                    mPointer = callee.getContext() ?? mPointer;
9,256✔
1207
                }
1208
                return this.inSubEnv((subInterpreter) => {
9,863✔
1209
                    subInterpreter.environment.setM(mPointer);
9,863✔
1210
                    this._stack.push({
9,863✔
1211
                        functionName: functionName,
1212
                        functionLocation: callee.getLocation() ?? this.location,
29,589✔
1213
                        callLocation: expression.callee.location,
1214
                        signature: signature,
1215
                    });
1216
                    try {
9,863✔
1217
                        const returnValue = callee.call(this, ...args);
9,863✔
1218
                        this._stack.pop();
9,739✔
1219
                        return returnValue;
9,739✔
1220
                    } catch (err) {
1221
                        this._stack.pop();
124✔
1222
                        throw err;
124✔
1223
                    }
1224
                });
1225
            } catch (reason) {
1226
                if (!(reason instanceof Stmt.BlockEnd)) {
124✔
1227
                    // re-throw interpreter errors
1228
                    throw reason;
15✔
1229
                }
1230

1231
                let returnedValue = (reason as Stmt.ReturnValue).value;
109✔
1232
                let returnLocation = (reason as Stmt.ReturnValue).location;
109✔
1233
                const signatureKind = satisfiedSignature.signature.returns;
109✔
1234

1235
                if (returnedValue && signatureKind === ValueKind.Void) {
109✔
1236
                    this.addError(
1✔
1237
                        new RuntimeError(RuntimeErrorDetail.ReturnWithValue, returnLocation)
1238
                    );
1239
                }
1240

1241
                if (!returnedValue && signatureKind !== ValueKind.Void) {
108✔
1242
                    this.addError(
1✔
1243
                        new RuntimeError(RuntimeErrorDetail.ReturnWithoutValue, returnLocation)
1244
                    );
1245
                }
1246

1247
                if (returnedValue) {
107✔
1248
                    let coercedValue = tryCoerce(returnedValue, signatureKind);
107✔
1249
                    if (coercedValue != null) {
107✔
1250
                        return coercedValue;
104✔
1251
                    }
1252
                }
1253

1254
                if (
3✔
1255
                    returnedValue &&
9✔
1256
                    signatureKind !== ValueKind.Dynamic &&
1257
                    signatureKind !== returnedValue.kind
1258
                ) {
1259
                    this.addError(
3✔
1260
                        new TypeMismatch({
1261
                            message: `Unable to cast`,
1262
                            left: {
1263
                                type: signatureKind,
1264
                                location: returnLocation,
1265
                            },
1266
                            right: {
1267
                                type: returnedValue,
1268
                                location: returnLocation,
1269
                            },
1270
                            cast: true,
1271
                        })
1272
                    );
1273
                }
1274

1275
                return returnedValue || BrsInvalid.Instance;
×
1276
            }
1277
        } else {
1278
            this.addError(
7✔
1279
                generateArgumentMismatchError(callee, args, expression.closingParen.location)
1280
            );
1281
        }
1282
    }
1283

1284
    visitDottedGet(expression: Expr.DottedGet) {
1285
        let source = this.evaluate(expression.obj);
10,009✔
1286

1287
        if (isIterable(source)) {
10,009✔
1288
            try {
9,409✔
1289
                const target = source.get(new BrsString(expression.name.text));
9,409✔
1290
                if (isBrsCallable(target) && source instanceof RoAssociativeArray) {
9,409✔
1291
                    target.setContext(source);
142✔
1292
                }
1293
                return target;
9,409✔
1294
            } catch (err: any) {
1295
                this.addError(new BrsError(err.message, expression.name.location));
×
1296
            }
1297
        }
1298

1299
        let boxedSource = isBoxable(source) ? source.box() : source;
600✔
1300
        if (boxedSource instanceof BrsComponent) {
600✔
1301
            // This check is supposed to be placed below the try/catch block,
1302
            // but it's here to mimic the behavior of Roku, if they fix, we move it.
1303
            if (source instanceof BrsInvalid && expression.optional) {
598✔
1304
                return source;
54✔
1305
            }
1306
            try {
544✔
1307
                const method = boxedSource.getMethod(expression.name.text);
544✔
1308
                if (method) {
544✔
1309
                    return method;
544✔
1310
                }
1311
            } catch (err: any) {
1312
                this.addError(new BrsError(err.message, expression.name.location));
×
1313
            }
1314
        }
1315
        this.addError(
2✔
1316
            new RuntimeError(RuntimeErrorDetail.DotOnNonObject, expression.name.location)
1317
        );
1318
    }
1319

1320
    visitIndexedGet(expression: Expr.IndexedGet): BrsType {
1321
        let source = this.evaluate(expression.obj);
8,205✔
1322
        if (!isIterable(source)) {
8,205✔
1323
            if (source instanceof BrsInvalid && expression.optional) {
5✔
1324
                return source;
5✔
1325
            }
UNCOV
1326
            this.addError(new RuntimeError(RuntimeErrorDetail.UndimmedArray, expression.location));
×
1327
        }
1328

1329
        if (
8,200✔
1330
            source instanceof RoAssociativeArray ||
24,586✔
1331
            source instanceof RoXMLElement ||
1332
            source instanceof RoSGNode
1333
        ) {
1334
            if (expression.indexes.length !== 1) {
8!
1335
                this.addError(
×
1336
                    new RuntimeError(
1337
                        RuntimeErrorDetail.WrongNumberOfParams,
1338
                        expression.closingSquare.location
1339
                    )
1340
                );
1341
            }
1342
            let index = this.evaluate(expression.indexes[0]);
8✔
1343
            if (!isBrsString(index)) {
8!
1344
                this.addError(
×
1345
                    new TypeMismatch({
1346
                        message: `"String" should be used as key, but received`,
1347
                        left: {
1348
                            type: index,
1349
                            location: expression.indexes[0].location,
1350
                        },
1351
                    })
1352
                );
1353
            }
1354
            try {
8✔
1355
                return source.get(index, true);
8✔
1356
            } catch (err: any) {
1357
                this.addError(new BrsError(err.message, expression.closingSquare.location));
×
1358
            }
1359
        }
1360
        if (source instanceof RoByteArray) {
8,192✔
1361
            if (expression.indexes.length !== 1) {
8,097!
1362
                this.addError(
×
1363
                    new RuntimeError(
1364
                        RuntimeErrorDetail.BadNumberOfIndexes,
1365
                        expression.closingSquare.location
1366
                    )
1367
                );
1368
            }
1369
        }
1370
        let current: BrsType = source;
8,192✔
1371
        for (let index of expression.indexes) {
8,192✔
1372
            let dimIndex = this.evaluate(index);
8,200✔
1373
            if (!isBrsNumber(dimIndex)) {
8,200✔
1374
                this.addError(
1✔
1375
                    new RuntimeError(RuntimeErrorDetail.NonNumericArrayIndex, index.location)
1376
                );
1377
            }
1378
            if (
8,199!
1379
                current instanceof RoArray ||
16,298✔
1380
                current instanceof RoByteArray ||
1381
                current instanceof RoList
1382
            ) {
1383
                try {
8,199✔
1384
                    current = current.get(dimIndex);
8,199✔
1385
                } catch (err: any) {
1386
                    this.addError(new BrsError(err.message, index.location));
×
1387
                }
1388
            } else {
1389
                this.addError(
×
1390
                    new RuntimeError(RuntimeErrorDetail.BadNumberOfIndexes, expression.location)
1391
                );
1392
            }
1393
        }
1394
        return current;
8,191✔
1395
    }
1396

1397
    visitGrouping(expr: Expr.Grouping) {
1398
        return this.evaluate(expr.expression);
28✔
1399
    }
1400

1401
    visitFor(statement: Stmt.For): BrsType {
1402
        // BrightScript for/to loops evaluate the counter initial value, final value, and increment
1403
        // values *only once*, at the top of the for/to loop.
1404
        this.execute(statement.counterDeclaration);
24✔
1405
        const startValue = this.evaluate(statement.counterDeclaration.value) as Int32 | Float;
24✔
1406
        const finalValue = this.evaluate(statement.finalValue) as Int32 | Float;
24✔
1407
        let increment = this.evaluate(statement.increment) as Int32 | Float;
24✔
1408
        if (increment instanceof Float) {
24✔
1409
            increment = new Int32(Math.trunc(increment.getValue()));
2✔
1410
        }
1411
        if (
24✔
1412
            (startValue.getValue() > finalValue.getValue() && increment.getValue() > 0) ||
70✔
1413
            (startValue.getValue() < finalValue.getValue() && increment.getValue() < 0)
1414
        ) {
1415
            // Shortcut, do not process anything
1416
            return BrsInvalid.Instance;
2✔
1417
        }
1418
        const counterName = statement.counterDeclaration.name;
22✔
1419
        const step = new Stmt.Assignment(
22✔
1420
            { equals: statement.tokens.for },
1421
            counterName,
1422
            new Expr.Binary(
1423
                new Expr.Variable(counterName),
1424
                {
1425
                    kind: Lexeme.Plus,
1426
                    text: "+",
1427
                    isReserved: false,
1428
                    location: {
1429
                        start: {
1430
                            line: -1,
1431
                            column: -1,
1432
                        },
1433
                        end: {
1434
                            line: -1,
1435
                            column: -1,
1436
                        },
1437
                        file: "(internal)",
1438
                    },
1439
                },
1440
                new Expr.Literal(increment, statement.increment.location)
1441
            )
1442
        );
1443

1444
        if (increment.getValue() > 0) {
22✔
1445
            while (
20✔
1446
                (this.evaluate(new Expr.Variable(counterName)) as Int32 | Float)
1447
                    .greaterThan(finalValue)
1448
                    .not()
1449
                    .toBoolean()
1450
            ) {
1451
                // execute the block
1452
                try {
8,286✔
1453
                    this.execute(statement.body);
8,286✔
1454
                } catch (reason) {
1455
                    if (reason instanceof Stmt.ExitForReason) {
9✔
1456
                        break;
2✔
1457
                    } else if (reason instanceof Stmt.ContinueForReason) {
7✔
1458
                        // continue to the next iteration
1459
                    } else {
1460
                        // re-throw returns, runtime errors, etc.
1461
                        throw reason;
1✔
1462
                    }
1463
                }
1464

1465
                // then increment the counter
1466
                this.execute(step);
8,283✔
1467
            }
1468
        } else {
1469
            while (
2✔
1470
                (this.evaluate(new Expr.Variable(counterName)) as Int32 | Float)
1471
                    .lessThan(finalValue)
1472
                    .not()
1473
                    .toBoolean()
1474
            ) {
1475
                // execute the block
1476
                try {
89✔
1477
                    this.execute(statement.body);
89✔
1478
                } catch (reason) {
1479
                    if (reason instanceof Stmt.ExitForReason) {
×
1480
                        break;
×
1481
                    } else if (reason instanceof Stmt.ContinueForReason) {
×
1482
                        // continue to the next iteration
1483
                    } else {
1484
                        // re-throw returns, runtime errors, etc.
1485
                        throw reason;
×
1486
                    }
1487
                }
1488

1489
                // then increment the counter
1490
                this.execute(step);
89✔
1491
            }
1492
        }
1493

1494
        return BrsInvalid.Instance;
21✔
1495
    }
1496

1497
    visitForEach(statement: Stmt.ForEach): BrsType {
1498
        let target = this.evaluate(statement.target);
21✔
1499
        if (!isIterable(target)) {
21!
1500
            // Roku device does not crash if the value is not iterable, just send a console message
1501
            const message = `BRIGHTSCRIPT: ERROR: Runtime: FOR EACH value is ${ValueKind.toString(
×
1502
                target.kind
1503
            )}`;
1504
            const location = `${statement.item.location.file}(${statement.item.location.start.line})`;
×
1505
            this.stderr.write(`${message}: ${location}\n`);
×
1506
            return BrsInvalid.Instance;
×
1507
        }
1508

1509
        target.getElements().every((element) => {
21✔
1510
            this.environment.define(Scope.Function, statement.item.text!, element);
70✔
1511

1512
            // execute the block
1513
            try {
70✔
1514
                this.execute(statement.body);
70✔
1515
            } catch (reason) {
1516
                if (reason instanceof Stmt.ExitForReason) {
5✔
1517
                    // break out of the loop
1518
                    return false;
1✔
1519
                } else if (reason instanceof Stmt.ContinueForReason) {
4!
1520
                    // continue to the next iteration
1521
                } else {
1522
                    // re-throw returns, runtime errors, etc.
1523
                    throw reason;
×
1524
                }
1525
            }
1526

1527
            // keep looping
1528
            return true;
69✔
1529
        });
1530

1531
        return BrsInvalid.Instance;
21✔
1532
    }
1533

1534
    visitWhile(statement: Stmt.While): BrsType {
1535
        while (this.evaluate(statement.condition).equalTo(BrsBoolean.True).toBoolean()) {
17✔
1536
            try {
63✔
1537
                this.execute(statement.body);
63✔
1538
            } catch (reason) {
1539
                if (reason instanceof Stmt.ExitWhileReason) {
9✔
1540
                    break;
2✔
1541
                } else if (reason instanceof Stmt.ContinueWhileReason) {
7✔
1542
                    // continue to the next iteration
1543
                } else {
1544
                    // re-throw returns, runtime errors, etc.
1545
                    throw reason;
1✔
1546
                }
1547
            }
1548
        }
1549

1550
        return BrsInvalid.Instance;
16✔
1551
    }
1552

1553
    visitIf(statement: Stmt.If): BrsType {
1554
        if (this.evaluate(statement.condition).equalTo(BrsBoolean.True).toBoolean()) {
8,405✔
1555
            this.execute(statement.thenBranch);
55✔
1556
            return BrsInvalid.Instance;
47✔
1557
        } else {
1558
            for (const elseIf of statement.elseIfs || []) {
8,350✔
1559
                if (this.evaluate(elseIf.condition).equalTo(BrsBoolean.True).toBoolean()) {
8✔
1560
                    this.execute(elseIf.thenBranch);
5✔
1561
                    return BrsInvalid.Instance;
5✔
1562
                }
1563
            }
1564

1565
            if (statement.elseBranch) {
8,345✔
1566
                this.execute(statement.elseBranch);
8✔
1567
            }
1568

1569
            return BrsInvalid.Instance;
8,344✔
1570
        }
1571
    }
1572

1573
    visitAnonymousFunction(func: Expr.Function): BrsType {
1574
        return toCallable(func);
45✔
1575
    }
1576

1577
    visitLiteral(expression: Expr.Literal): BrsType {
1578
        return expression.value;
12,804✔
1579
    }
1580

1581
    visitArrayLiteral(expression: Expr.ArrayLiteral): RoArray {
1582
        return new RoArray(expression.elements.map((expr) => this.evaluate(expr)));
164✔
1583
    }
1584

1585
    visitAALiteral(expression: Expr.AALiteral): BrsType {
1586
        return new RoAssociativeArray(
91✔
1587
            expression.elements.map((member) => ({
132✔
1588
                name: member.name,
1589
                value: this.evaluate(member.value),
1590
            }))
1591
        );
1592
    }
1593

1594
    visitDottedSet(statement: Stmt.DottedSet) {
1595
        let source = this.evaluate(statement.obj);
147✔
1596
        let value = this.evaluate(statement.value);
147✔
1597

1598
        if (!isIterable(source)) {
147!
1599
            this.addError(new RuntimeError(RuntimeErrorDetail.BadLHS, statement.name.location));
×
1600
        }
1601

1602
        try {
147✔
1603
            source.set(new BrsString(statement.name.text), value);
147✔
1604
        } catch (err: any) {
1605
            this.addError(new BrsError(err.message, statement.name.location));
×
1606
        }
1607

1608
        return BrsInvalid.Instance;
147✔
1609
    }
1610

1611
    visitIndexedSet(statement: Stmt.IndexedSet) {
1612
        let value = this.evaluate(statement.value);
22✔
1613
        let source = this.evaluate(statement.obj);
22✔
1614

1615
        if (!isIterable(source)) {
22!
1616
            this.addError(new RuntimeError(RuntimeErrorDetail.BadLHS, statement.obj.location));
×
1617
        }
1618

1619
        if (
22✔
1620
            source instanceof RoAssociativeArray ||
44✔
1621
            source instanceof RoXMLElement ||
1622
            source instanceof RoSGNode
1623
        ) {
1624
            if (statement.indexes.length !== 1) {
11!
1625
                this.addError(
×
1626
                    new RuntimeError(
1627
                        RuntimeErrorDetail.WrongNumberOfParams,
1628
                        statement.closingSquare.location
1629
                    )
1630
                );
1631
            }
1632
            let index = this.evaluate(statement.indexes[0]);
11✔
1633
            if (!isBrsString(index)) {
11!
1634
                this.addError(
×
1635
                    new TypeMismatch({
1636
                        message: `"String" should be used as key, but received`,
1637
                        left: {
1638
                            type: index,
1639
                            location: statement.indexes[0].location,
1640
                        },
1641
                    })
1642
                );
1643
            }
1644
            try {
11✔
1645
                source.set(index, value, true);
11✔
1646
            } catch (err: any) {
1647
                this.addError(new BrsError(err.message, statement.closingSquare.location));
×
1648
            }
1649
            return BrsInvalid.Instance;
11✔
1650
        }
1651
        if (source instanceof RoByteArray) {
11!
1652
            if (statement.indexes.length !== 1) {
×
1653
                this.addError(
×
1654
                    new RuntimeError(
1655
                        RuntimeErrorDetail.BadNumberOfIndexes,
1656
                        statement.closingSquare.location
1657
                    )
1658
                );
1659
            }
1660
        }
1661

1662
        let current: BrsType = source;
11✔
1663
        for (let i = 0; i < statement.indexes.length; i++) {
11✔
1664
            let index = this.evaluate(statement.indexes[i]);
12✔
1665
            if (!isBrsNumber(index)) {
12!
1666
                this.addError(
×
1667
                    new RuntimeError(
1668
                        RuntimeErrorDetail.NonNumericArrayIndex,
1669
                        statement.indexes[i].location
1670
                    )
1671
                );
1672
            }
1673

1674
            if (i < statement.indexes.length - 1) {
12✔
1675
                if (
1!
1676
                    current instanceof RoArray ||
1!
1677
                    current instanceof RoByteArray ||
1678
                    current instanceof RoList
1679
                ) {
1680
                    try {
1✔
1681
                        current = current.get(index);
1✔
1682
                    } catch (err: any) {
1683
                        this.addError(new BrsError(err.message, statement.closingSquare.location));
×
1684
                    }
1685
                } else {
1686
                    this.addError(
×
1687
                        new RuntimeError(RuntimeErrorDetail.BadNumberOfIndexes, statement.location)
1688
                    );
1689
                }
1690
            } else if (
11!
1691
                current instanceof RoArray ||
11!
1692
                current instanceof RoByteArray ||
1693
                current instanceof RoList
1694
            ) {
1695
                try {
11✔
1696
                    current.set(index, value);
11✔
1697
                } catch (err: any) {
1698
                    this.addError(new BrsError(err.message, statement.closingSquare.location));
×
1699
                }
1700
            } else {
1701
                this.addError(
×
1702
                    new RuntimeError(RuntimeErrorDetail.BadNumberOfIndexes, statement.location)
1703
                );
1704
            }
1705
        }
1706

1707
        return BrsInvalid.Instance;
11✔
1708
    }
1709

1710
    visitIncrement(statement: Stmt.Increment) {
1711
        let target = this.evaluate(statement.value);
259✔
1712
        if (isBoxedNumber(target)) {
259✔
1713
            target = target.unbox();
1✔
1714
        }
1715

1716
        if (!isBrsNumber(target)) {
259!
1717
            let operation = statement.token.kind === Lexeme.PlusPlus ? "increment" : "decrement";
×
1718
            this.addError(
×
1719
                new TypeMismatch({
1720
                    message: `Attempting to ${operation} value of non-numeric type`,
1721
                    left: {
1722
                        type: target,
1723
                        location: statement.location,
1724
                    },
1725
                })
1726
            );
1727
        }
1728

1729
        let result: BrsNumber;
1730
        if (statement.token.kind === Lexeme.PlusPlus) {
259✔
1731
            result = target.add(new Int32(1));
256✔
1732
        } else {
1733
            result = target.subtract(new Int32(1));
3✔
1734
        }
1735

1736
        if (statement.value instanceof Expr.Variable) {
259✔
1737
            // store the result of the operation
1738
            this.environment.define(Scope.Function, statement.value.name.text, result);
255✔
1739
        } else if (statement.value instanceof Expr.DottedGet) {
4✔
1740
            // immediately execute a dotted "set" statement
1741
            this.execute(
2✔
1742
                new Stmt.DottedSet(
1743
                    statement.value.obj,
1744
                    statement.value.name,
1745
                    new Expr.Literal(result, statement.location)
1746
                )
1747
            );
1748
        } else if (statement.value instanceof Expr.IndexedGet) {
2✔
1749
            // immediately execute an indexed "set" statement
1750
            this.execute(
2✔
1751
                new Stmt.IndexedSet(
1752
                    statement.value.obj,
1753
                    statement.value.indexes,
1754
                    new Expr.Literal(result, statement.location),
1755
                    statement.value.closingSquare
1756
                )
1757
            );
1758
        }
1759

1760
        // always return `invalid`, because ++/-- are purely side-effects in BrightScript
1761
        return BrsInvalid.Instance;
259✔
1762
    }
1763

1764
    visitUnary(expression: Expr.Unary) {
1765
        let right = this.evaluate(expression.right);
75✔
1766
        if (isBoxedNumber(right)) {
75✔
1767
            right = right.unbox();
1✔
1768
        }
1769

1770
        switch (expression.operator.kind) {
75✔
1771
            case Lexeme.Minus:
1772
                if (isBrsNumber(right)) {
46✔
1773
                    return right.multiply(new Int32(-1));
45✔
1774
                } else {
1775
                    return this.addError(
1✔
1776
                        new BrsError(
1777
                            `Attempting to negate non-numeric value.
1778
                            value type: ${ValueKind.toString(right.kind)}`,
1779
                            expression.operator.location
1780
                        )
1781
                    );
1782
                }
1783
            case Lexeme.Plus:
1784
                if (isBrsNumber(right)) {
8!
1785
                    return right;
8✔
1786
                } else {
1787
                    return this.addError(
×
1788
                        new BrsError(
1789
                            `Attempting to apply unary positive operator to non-numeric value.
1790
                            value type: ${ValueKind.toString(right.kind)}`,
1791
                            expression.operator.location
1792
                        )
1793
                    );
1794
                }
1795
            case Lexeme.Not:
1796
                if (isBrsBoolean(right) || isBrsNumber(right)) {
21!
1797
                    return right.not();
21✔
1798
                } else {
1799
                    return this.addError(
×
1800
                        new BrsError(
1801
                            `Type Mismatch. Operator "not" can't be applied to "${ValueKind.toString(
1802
                                right.kind
1803
                            )}".`,
1804
                            expression.operator.location
1805
                        )
1806
                    );
1807
                }
1808
        }
1809

1810
        return BrsInvalid.Instance;
×
1811
    }
1812

1813
    visitVariable(expression: Expr.Variable) {
1814
        try {
54,752✔
1815
            return this.environment.get(expression.name);
54,752✔
1816
        } catch (err) {
1817
            if (err instanceof NotFound) {
1,983✔
1818
                return Uninitialized.Instance;
1,983✔
1819
            }
1820

1821
            throw err;
×
1822
        }
1823
    }
1824

1825
    evaluate(this: Interpreter, expression: Expr.Expression): BrsType {
1826
        this.location = expression.location;
114,069✔
1827
        this.reportCoverageHit(expression);
114,069✔
1828

1829
        return expression.accept<BrsType>(this);
114,069✔
1830
    }
1831

1832
    execute(this: Interpreter, statement: Stmt.Statement): BrsType {
1833
        this.location = statement.location;
36,899✔
1834
        this.reportCoverageHit(statement);
36,899✔
1835

1836
        return statement.accept<BrsType>(this);
36,899✔
1837
    }
1838

1839
    /**
1840
     * Returns the Backtrace formatted as an array for Try/Catch
1841
     * @param loc the location of the error
1842
     * @param bt the backtrace array, default is current stack trace
1843
     * @returns an array with the backtrace formatted
1844
     */
1845
    formatBacktrace(loc: Location, bt?: TracePoint[]): RoArray {
1846
        const backTrace = bt ?? this._stack;
4!
1847
        const btArray: BrsType[] = [];
4✔
1848
        for (let index = backTrace.length - 1; index >= 0; index--) {
4✔
1849
            const func = backTrace[index];
7✔
1850
            if (func.signature) {
7✔
1851
                const kind = ValueKind.toString(func.signature.returns);
7✔
1852
                let args = "";
7✔
1853
                func.signature.args.forEach((arg) => {
7✔
1854
                    args += args !== "" ? "," : "";
3!
1855
                    args += `${arg.name.text} As ${ValueKind.toString(arg.type.kind)}`;
3✔
1856
                });
1857
                const funcSignature = `${func.functionName}(${args}) As ${kind}`;
7✔
1858
                const line = loc.start.line;
7✔
1859
                btArray.unshift(
7✔
1860
                    new RoAssociativeArray([
1861
                        {
1862
                            name: new BrsString("filename"),
1863
                            value: new BrsString(loc?.file ?? "()"),
42!
1864
                        },
1865
                        { name: new BrsString("function"), value: new BrsString(funcSignature) },
1866
                        { name: new BrsString("line_number"), value: new Int32(line) },
1867
                    ])
1868
                );
1869
                loc = func.callLocation;
7✔
1870
            }
1871
        }
1872
        return new RoArray(btArray);
4✔
1873
    }
1874

1875
    /**
1876
     * Method to return the current scope of the interpreter for the REPL and Micro Debugger
1877
     * @returns a string representation of the local variables in the current scope
1878
     */
1879
    formatLocalVariables(): string {
1880
        let debugMsg = `${"global".padEnd(16)} Interface:ifGlobal\r\n`;
×
1881
        debugMsg += `${"m".padEnd(16)} roAssociativeArray count:${
×
1882
            this.environment.getM().getElements().length
1883
        }\r\n`;
1884
        let fnc = this.environment.getList(Scope.Function);
×
1885
        fnc.forEach((value, key) => {
×
1886
            const varName = key.padEnd(17);
×
1887
            if (PrimitiveKinds.has(value.kind)) {
×
1888
                let text = value.toString();
×
1889
                let lf = text.length <= 94 ? "\r\n" : "...\r\n";
×
1890
                if (value.kind === ValueKind.String) {
×
1891
                    text = `"${text.substring(0, 94)}"`;
×
1892
                }
1893
                debugMsg += `${varName}${ValueKind.toString(value.kind)} val:${text}${lf}`;
×
1894
            } else if (isIterable(value)) {
×
1895
                const count = value.getElements().length;
×
1896
                debugMsg += `${varName}${value.getComponentName()} count:${count}\r\n`;
×
1897
            } else if (value instanceof BrsComponent && isUnboxable(value)) {
×
1898
                const unboxed = value.unbox();
×
1899
                debugMsg += `${varName}${value.getComponentName()} val:${unboxed.toString()}\r\n`;
×
1900
            } else if (value.kind === ValueKind.Object) {
×
1901
                debugMsg += `${varName}${value.getComponentName()}\r\n`;
×
1902
            } else if (value.kind === ValueKind.Callable) {
×
1903
                debugMsg += `${varName}${ValueKind.toString(
×
1904
                    value.kind
1905
                )} val:${value.getName()}\r\n`;
1906
            } else {
1907
                debugMsg += `${varName}${value.toString().substring(0, 94)}\r\n`;
×
1908
            }
1909
        });
1910
        return debugMsg;
×
1911
    }
1912

1913
    /** Method to return a string with the current source code location
1914
     * @returns a string representation of the location
1915
     */
1916
    formatLocation(location: Location = this.location) {
3✔
1917
        let formattedLocation: string;
1918
        if (location.start.line) {
3!
1919
            formattedLocation = `pkg:/${location.file}(${location.start.line})`;
3✔
1920
        } else {
1921
            formattedLocation = `pkg:/${location.file}(??)`;
×
1922
        }
1923
        return formattedLocation;
3✔
1924
    }
1925

1926
    /**
1927
     * Emits an error via this processor's `events` property, then throws it.
1928
     * @param err the ParseError to emit then throw
1929
     */
1930
    public addError(err: BrsError): never {
1931
        if (!err.backTrace) {
117✔
1932
            err.backTrace = this._stack.slice();
117✔
1933
        }
1934
        if (!this._tryMode) {
117✔
1935
            // do not save/emit the error if we are in a try block
1936
            this.errors.push(err);
115✔
1937
            this.events.emit("err", err);
115✔
1938
        }
1939
        throw err;
117✔
1940
    }
1941

1942
    private isPositive(value: number | Long): boolean {
1943
        if (value instanceof Long) {
358✔
1944
            return value.isPositive();
9✔
1945
        }
1946
        return value >= 0;
349✔
1947
    }
1948

1949
    private lessThan(value: number | Long, compare: number): boolean {
1950
        if (value instanceof Long) {
18!
1951
            return value.lessThan(compare);
×
1952
        }
1953
        return value < compare;
18✔
1954
    }
1955
}
1956

1957
/**
1958
 * Colorizes the console messages.
1959
 *
1960
 */
1961
export function colorize(log: string) {
135✔
1962
    return log
1,712✔
1963
        .replace(/\b(down|error|errors|failure|fail|fatal|false)(:|\b)/gi, chalk.red("$1$2"))
1964
        .replace(/\b(warning|warn|test|null|undefined|invalid)(:|\b)/gi, chalk.yellow("$1$2"))
1965
        .replace(/\b(help|hint|info|information|true|log)(:|\b)/gi, chalk.cyan("$1$2"))
1966
        .replace(/\b(running|success|successfully|valid)(:|\b)/gi, chalk.green("$1$2"))
1967
        .replace(/\b(debug|roku|brs|brightscript)(:|\b)/gi, chalk.magenta("$1$2"))
1968
        .replace(/(\b\d+\.?\d*?\b)/g, chalk.ansi256(122)(`$1`)) // Numeric
1969
        .replace(/\S+@\S+\.\S+/g, (match: string) => {
1970
            return chalk.blueBright(stripAnsi(match)); // E-Mail
1✔
1971
        })
1972
        .replace(/\b([a-z]+):\/{1,2}[^\/].*/gi, (match: string) => {
1973
            return chalk.blue.underline(stripAnsi(match)); // URL
5✔
1974
        })
1975
        .replace(/<(.*?)>/g, (match: string) => {
1976
            return chalk.greenBright(stripAnsi(match)); // Delimiters < >
21✔
1977
        })
1978
        .replace(/"(.*?)"/g, (match: string) => {
1979
            return chalk.ansi256(222)(stripAnsi(match)); // Quotes
30✔
1980
        });
1981
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc