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

rokucommunity / brs / #305

18 Jan 2024 09:05PM UTC coverage: 91.463% (+5.2%) from 86.214%
#305

push

TwitchBronBron
0.45.4

1796 of 2095 branches covered (85.73%)

Branch coverage included in aggregate %.

5275 of 5636 relevant lines covered (93.59%)

8947.19 hits per line

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

90.05
/src/parser/Parser.ts
1
import { EventEmitter } from "events";
139✔
2

3
import * as Expr from "./Expression";
139✔
4
type Expression = Expr.Expression;
5
import * as Stmt from "./Statement";
139✔
6
type Statement = Stmt.Statement;
7
import { Lexeme, Token, Identifier, Location, ReservedWords } from "../lexer";
139✔
8
import { ParseError } from "./ParseError";
139✔
9

10
import {
139✔
11
    BrsInvalid,
12
    BrsBoolean,
13
    BrsString,
14
    Int32,
15
    ValueKind,
16
    Argument,
17
    StdlibArgument,
18
} from "../brsTypes";
19

20
/** Set of all keywords that end blocks. */
21
type BlockTerminator =
22
    | Lexeme.ElseIf
23
    | Lexeme.Else
24
    | Lexeme.EndFor
25
    | Lexeme.Next
26
    | Lexeme.EndIf
27
    | Lexeme.EndWhile
28
    | Lexeme.EndSub
29
    | Lexeme.EndFunction
30
    | Lexeme.Newline // possible only in a single-line `if` statement
31
    | Lexeme.Eof // possible only in a single-line `if` statement
32
    | Lexeme.Catch
33
    | Lexeme.EndTry;
34

35
/** The set of operators valid for use in assignment statements. */
36
const assignmentOperators = [
139✔
37
    Lexeme.Equal,
38
    Lexeme.MinusEqual,
39
    Lexeme.PlusEqual,
40
    Lexeme.StarEqual,
41
    Lexeme.SlashEqual,
42
    Lexeme.BackslashEqual,
43
    Lexeme.LeftShiftEqual,
44
    Lexeme.RightShiftEqual,
45
];
46

47
/** List of Lexemes that are permitted as property names. */
48
const allowedProperties = [
139✔
49
    Lexeme.And,
50
    Lexeme.Box,
51
    Lexeme.CreateObject,
52
    Lexeme.Dim,
53
    Lexeme.Else,
54
    Lexeme.ElseIf,
55
    Lexeme.End,
56
    Lexeme.EndFunction,
57
    Lexeme.EndFor,
58
    Lexeme.EndIf,
59
    Lexeme.EndSub,
60
    Lexeme.EndWhile,
61
    Lexeme.Eval,
62
    Lexeme.Exit,
63
    Lexeme.ExitFor,
64
    Lexeme.ExitWhile,
65
    Lexeme.False,
66
    Lexeme.For,
67
    Lexeme.ForEach,
68
    Lexeme.Function,
69
    Lexeme.GetGlobalAA,
70
    Lexeme.GetLastRunCompileError,
71
    Lexeme.GetLastRunRunTimeError,
72
    Lexeme.Goto,
73
    Lexeme.If,
74
    Lexeme.Invalid,
75
    Lexeme.Let,
76
    Lexeme.Next,
77
    Lexeme.Not,
78
    Lexeme.ObjFun,
79
    Lexeme.Or,
80
    Lexeme.Pos,
81
    Lexeme.Print,
82
    Lexeme.Rem,
83
    Lexeme.Return,
84
    Lexeme.Step,
85
    Lexeme.Stop,
86
    Lexeme.Sub,
87
    Lexeme.Tab,
88
    Lexeme.To,
89
    Lexeme.True,
90
    Lexeme.Type,
91
    Lexeme.While,
92
];
93

94
/** List of Lexeme that are allowed as local var identifiers. */
95
const allowedIdentifiers = [
139✔
96
    Lexeme.EndFor,
97
    Lexeme.ExitFor,
98
    Lexeme.ForEach,
99
    Lexeme.Try,
100
    Lexeme.Catch,
101
];
102

103
/**
104
 * List of string versions of Lexeme that are NOT allowed as local var identifiers.
105
 * Used to throw more helpful "you can't use a reserved word as an identifier" errors.
106
 */
107
const disallowedIdentifiers = new Set(
139✔
108
    [
109
        Lexeme.And,
110
        Lexeme.Box,
111
        Lexeme.CreateObject,
112
        Lexeme.Dim,
113
        Lexeme.Else,
114
        Lexeme.ElseIf,
115
        Lexeme.End,
116
        Lexeme.EndFunction,
117
        Lexeme.EndIf,
118
        Lexeme.EndSub,
119
        Lexeme.EndWhile,
120
        Lexeme.Eval,
121
        Lexeme.Exit,
122
        Lexeme.ExitWhile,
123
        Lexeme.False,
124
        Lexeme.For,
125
        Lexeme.Function,
126
        Lexeme.GetGlobalAA,
127
        Lexeme.GetLastRunCompileError,
128
        Lexeme.GetLastRunRunTimeError,
129
        Lexeme.Goto,
130
        Lexeme.If,
131
        Lexeme.Invalid,
132
        Lexeme.Let,
133
        Lexeme.Next,
134
        Lexeme.Not,
135
        Lexeme.ObjFun,
136
        Lexeme.Or,
137
        Lexeme.Pos,
138
        Lexeme.Print,
139
        Lexeme.Rem,
140
        Lexeme.Return,
141
        Lexeme.Step,
142
        Lexeme.Sub,
143
        Lexeme.Tab,
144
        Lexeme.To,
145
        Lexeme.True,
146
        Lexeme.Type,
147
        Lexeme.While,
148
    ].map((x) => Lexeme[x].toLowerCase())
5,421✔
149
);
150

151
/** The results of a Parser's parsing pass. */
152
interface ParseResults {
153
    /** The statements produced by the parser. */
154
    statements: Stmt.Statement[];
155
    /** The errors encountered by the Parser. */
156
    errors: ParseError[];
157
}
158

159
export class Parser {
139✔
160
    /** Allows consumers to observe errors as they're detected. */
161
    readonly events = new EventEmitter();
1,674✔
162

163
    /**
164
     * A convenience function, equivalent to `new Parser().parse(toParse)`, that parses an array of
165
     * `Token`s into an abstract syntax tree that can be executed with the `Interpreter`.
166
     * @param toParse the array of tokens to parse
167
     * @returns an array of `Statement` objects that together form the abstract syntax tree of the
168
     *          program
169
     */
170
    static parse(toParse: ReadonlyArray<Token>) {
171
        return new Parser().parse(toParse);
24✔
172
    }
173

174
    /**
175
     * Convenience function to subscribe to the `err` events emitted by `parser.events`.
176
     * @param errorHandler the function to call for every Parser error emitted after subscribing
177
     * @returns an object with a `dispose` function, used to unsubscribe from errors
178
     */
179
    public onError(errorHandler: (err: ParseError) => void) {
180
        this.events.on("err", errorHandler);
1,460✔
181
        return {
1,460✔
182
            dispose: () => {
183
                this.events.removeListener("err", errorHandler);
×
184
            },
185
        };
186
    }
187

188
    /**
189
     * Convenience function to subscribe to a single `err` event emitted by `parser.events`.
190
     * @param errorHandler the function to call for the first Parser error emitted after subscribing
191
     */
192
    public onErrorOnce(errorHandler: (err: ParseError) => void) {
193
        this.events.once("err", errorHandler);
×
194
    }
195

196
    /**
197
     * Parses an array of `Token`s into an abstract syntax tree that can be executed with the `Interpreter`.
198
     * @param toParse the array of tokens to parse
199
     * @returns an array of `Statement` objects that together form the abstract syntax tree of the
200
     *          program
201
     */
202
    parse(toParse: ReadonlyArray<Token>): ParseResults {
203
        let current = 0;
1,660✔
204
        let tokens = toParse;
1,660✔
205

206
        //the depth of the calls to function declarations. Helps some checks know if they are at the root or not.
207
        let functionDeclarationLevel = 0;
1,660✔
208

209
        function isAtRootLevel() {
210
            return functionDeclarationLevel === 0;
40,204✔
211
        }
212

213
        let statements: Statement[] = [];
1,660✔
214

215
        let errors: ParseError[] = [];
1,660✔
216

217
        /**
218
         * Add an error to the parse results.
219
         * @param token - the token where the error occurred
220
         * @param message - the message for this error
221
         * @returns an error object that can be thrown if the calling code needs to abort parsing
222
         */
223
        const addError = (token: Token, message: string) => {
1,660✔
224
            let err = new ParseError(token, message);
40✔
225
            errors.push(err);
40✔
226
            this.events.emit("err", err);
40✔
227
            return err;
40✔
228
        };
229

230
        /**
231
         * Add an error at the given location.
232
         * @param location
233
         * @param message
234
         */
235
        const addErrorAtLocation = (location: Location, message: string) => {
1,660✔
236
            addError({ location: location } as any, message);
6✔
237
        };
238

239
        if (toParse.length === 0) {
1,660!
240
            return {
×
241
                statements: [],
242
                errors: [],
243
            };
244
        }
245

246
        try {
1,660✔
247
            while (!isAtEnd()) {
1,660✔
248
                let dec = declaration();
3,914✔
249
                if (dec) {
3,914✔
250
                    statements.push(dec);
3,900✔
251
                }
252
            }
253

254
            return { statements, errors };
1,659✔
255
        } catch (parseError) {
256
            return {
1✔
257
                statements: [],
258
                errors: errors,
259
            };
260
        }
261

262
        /**
263
         * A simple wrapper around `check` to make tests for a `end` identifier.
264
         * `end` is a keyword, but not reserved, so associative arrays can have properties
265
         * called `end`; the parser takes on this task.
266
         * @returns `true` if the next token is an identifier with text `end`, otherwise `false`
267
         */
268
        function checkEnd() {
269
            return check(Lexeme.Identifier) && peek().text.toLowerCase() === "end";
8,633✔
270
        }
271

272
        function declaration(...additionalTerminators: BlockTerminator[]): Statement | undefined {
273
            try {
25,226✔
274
                let statementSeparators = [Lexeme.Colon];
25,226✔
275

276
                if (additionalTerminators.includes(Lexeme.Newline)) {
25,226✔
277
                    // if this declaration can be terminated with a newline, check for one
278
                    // before other statement separators
279
                    if (check(Lexeme.Newline)) {
42!
280
                        return;
×
281
                    }
282
                } else {
283
                    statementSeparators.push(Lexeme.Newline);
25,184✔
284
                }
285

286
                while (match(...statementSeparators));
25,226✔
287

288
                if (additionalTerminators.includes(Lexeme.Newline)) {
25,226✔
289
                    // if this declaration can be terminated with a newline, check for one
290
                    // _after_ a series of `:`s.
291
                    if (check(Lexeme.Newline)) {
42!
292
                        return;
×
293
                    }
294
                }
295

296
                // if we reached the end, don't attempt to do anything else
297
                if (isAtEnd()) {
25,226!
298
                    return;
×
299
                }
300

301
                try {
25,226✔
302
                    if (functionDeclarationLevel === 0 && check(Lexeme.Sub, Lexeme.Function)) {
25,226✔
303
                        return functionDeclaration(false);
3,551✔
304
                    }
305

306
                    if (checkLibrary()) {
21,675✔
307
                        return libraryStatement();
7✔
308
                    }
309

310
                    // BrightScript is like python, in that variables can be declared without a `var`,
311
                    // `let`, (...) keyword. As such, we must check the token *after* an identifier to figure
312
                    // out what to do with it.
313
                    if (
21,668✔
314
                        check(Lexeme.Identifier, ...allowedIdentifiers) &&
32,373✔
315
                        checkNext(...assignmentOperators)
316
                    ) {
317
                        return assignment(...additionalTerminators);
3,146✔
318
                    }
319

320
                    return statement(...additionalTerminators);
18,522✔
321
                } finally {
322
                    while (match(...statementSeparators));
25,226✔
323
                }
324
            } catch (error) {
325
                synchronize();
21✔
326
                return;
21✔
327
            }
328
        }
329

330
        function functionDeclaration(isAnonymous: true): Expr.Function;
331
        function functionDeclaration(isAnonymous: false): Stmt.Function;
332
        function functionDeclaration(isAnonymous: boolean) {
333
            try {
3,785✔
334
                //certain statements need to know if they are contained within a function body
335
                //so track the depth here
336
                functionDeclarationLevel++;
3,785✔
337
                let startingKeyword = peek();
3,785✔
338
                let isSub = check(Lexeme.Sub);
3,785✔
339
                let functionType = advance();
3,785✔
340
                let name: Identifier;
341
                let returnType: ValueKind;
342
                let leftParen: Token;
343
                let rightParen: Token;
344

345
                if (isSub) {
3,785✔
346
                    returnType = ValueKind.Void;
2,490✔
347
                } else {
348
                    returnType = ValueKind.Dynamic;
1,295✔
349
                }
350

351
                if (isAnonymous) {
3,785✔
352
                    leftParen = consume(
234✔
353
                        `Expected '(' after ${functionType.text}`,
354
                        Lexeme.LeftParen
355
                    );
356
                } else {
357
                    name = consume(
3,551✔
358
                        `Expected ${functionType.text} name after '${functionType.text}'`,
359
                        Lexeme.Identifier
360
                    ) as Identifier;
361
                    leftParen = consume(
3,551✔
362
                        `Expected '(' after ${functionType.text} name`,
363
                        Lexeme.LeftParen
364
                    );
365

366
                    //prevent functions from ending with type designators
367
                    let lastChar = name.text[name.text.length - 1];
3,551✔
368
                    if (["$", "%", "!", "#", "&"].includes(lastChar)) {
3,551✔
369
                        //don't throw this error; let the parser continue
370
                        addError(
8✔
371
                            name,
372
                            `Function name '${name.text}' cannot end with type designator '${lastChar}'`
373
                        );
374
                    }
375
                }
376

377
                let args: Argument[] = [];
3,785✔
378
                if (!check(Lexeme.RightParen)) {
3,785✔
379
                    do {
1,010✔
380
                        if (args.length >= Expr.Call.MaximumArguments) {
1,440!
381
                            throw addError(
×
382
                                peek(),
383
                                `Cannot have more than ${Expr.Call.MaximumArguments} arguments`
384
                            );
385
                        }
386

387
                        args.push(signatureArgument());
1,440✔
388
                    } while (match(Lexeme.Comma));
389
                }
390
                rightParen = advance();
3,785✔
391

392
                let maybeAs = peek();
3,785✔
393
                if (check(Lexeme.Identifier) && maybeAs.text.toLowerCase() === "as") {
3,785✔
394
                    advance();
649✔
395

396
                    let typeToken = advance();
649✔
397
                    let typeString = typeToken.text || "";
649!
398
                    let maybeReturnType = ValueKind.fromString(typeString);
649✔
399

400
                    if (!maybeReturnType) {
649!
401
                        throw addError(
×
402
                            typeToken,
403
                            `Function return type '${typeString}' is invalid`
404
                        );
405
                    }
406

407
                    returnType = maybeReturnType;
649✔
408
                }
409

410
                args.reduce((haveFoundOptional: boolean, arg: Argument) => {
3,785✔
411
                    if (haveFoundOptional && !arg.defaultValue) {
1,440!
412
                        throw addError(
×
413
                            {
414
                                kind: Lexeme.Identifier,
415
                                text: arg.name.text,
416
                                isReserved: ReservedWords.has(arg.name.text),
417
                                location: arg.location,
418
                            },
419
                            `Argument '${arg.name.text}' has no default value, but comes after arguments with default values`
420
                        );
421
                    }
422

423
                    return haveFoundOptional || !!arg.defaultValue;
1,440✔
424
                }, false);
425

426
                checkOrThrow(
3,785✔
427
                    `Expected newline or ':' after ${functionType.text} signature`,
428
                    Lexeme.Newline,
429
                    Lexeme.Colon
430
                );
431
                //support ending the function with `end sub` OR `end function`
432
                let maybeBody = block(Lexeme.EndSub, Lexeme.EndFunction);
3,785✔
433
                if (!maybeBody) {
3,785!
434
                    throw addError(
×
435
                        peek(),
436
                        `Expected 'end ${functionType.text}' to terminate ${functionType.text} block`
437
                    );
438
                }
439
                let endingKeyword = maybeBody.closingToken;
3,785✔
440
                let expectedEndKind = isSub ? Lexeme.EndSub : Lexeme.EndFunction;
3,785✔
441

442
                //if `function` is ended with `end sub`, or `sub` is ended with `end function`, then
443
                //add an error but don't hard-fail so the AST can continue more gracefully
444
                if (endingKeyword.kind !== expectedEndKind) {
3,785✔
445
                    addError(
2✔
446
                        endingKeyword,
447
                        `Expected 'end ${functionType.text}' to terminate ${functionType.text} block`
448
                    );
449
                }
450

451
                let func = new Expr.Function(
3,785✔
452
                    args,
453
                    returnType,
454
                    maybeBody.body,
455
                    startingKeyword,
456
                    endingKeyword
457
                );
458

459
                if (isAnonymous) {
3,785✔
460
                    return func;
234✔
461
                } else {
462
                    // only consume trailing newlines in the statement context; expressions
463
                    // expect to handle their own trailing whitespace
464
                    while (match(Lexeme.Newline));
3,551✔
465
                    return new Stmt.Function(name!, func);
3,551✔
466
                }
467
            } finally {
468
                functionDeclarationLevel--;
3,785✔
469
            }
470
        }
471

472
        function signatureArgument(): Argument {
473
            if (!check(Lexeme.Identifier)) {
1,440!
474
                throw addError(
×
475
                    peek(),
476
                    `Expected argument name, but received '${peek().text || ""}'`
×
477
                );
478
            }
479

480
            let name = advance();
1,440✔
481
            let type: ValueKind = ValueKind.Dynamic;
1,440✔
482
            let typeToken: Token | undefined;
483
            let defaultValue;
484

485
            // parse argument default value
486
            if (match(Lexeme.Equal)) {
1,440✔
487
                // it seems any expression is allowed here -- including ones that operate on other arguments!
488
                defaultValue = expression();
80✔
489
            }
490

491
            let next = peek();
1,440✔
492
            if (check(Lexeme.Identifier) && next.text && next.text.toLowerCase() === "as") {
1,440✔
493
                // 'as' isn't a reserved word, so it can't be lexed into an As token without the lexer
494
                // understanding language context.  That's less than ideal, so we'll have to do some
495
                // more intelligent comparisons to detect the 'as' sometimes-keyword here.
496
                advance();
1,124✔
497

498
                typeToken = advance();
1,124✔
499
                let typeValueKind = ValueKind.fromString(typeToken.text);
1,124✔
500

501
                if (!typeValueKind) {
1,124!
502
                    throw addError(
×
503
                        typeToken,
504
                        `Function parameter '${name.text}' is of invalid type '${typeToken.text}'`
505
                    );
506
                }
507

508
                type = typeValueKind;
1,124✔
509
            }
510

511
            return {
1,440✔
512
                name: name,
513
                type: {
514
                    kind: type,
515
                    location: typeToken ? typeToken.location : StdlibArgument.InternalLocation,
1,440✔
516
                },
517
                defaultValue: defaultValue,
518
                location: {
519
                    file: name.location.file,
520
                    start: name.location.start,
521
                    end: typeToken ? typeToken.location.end : name.location.end,
1,440✔
522
                },
523
            };
524
        }
525

526
        function assignment(...additionalterminators: Lexeme[]): Stmt.Assignment {
527
            let name = advance() as Identifier;
3,164✔
528
            //add error if name is a reserved word that cannot be used as an identifier
529
            if (disallowedIdentifiers.has(name.text.toLowerCase())) {
3,164✔
530
                //don't throw...this is fully recoverable
531
                addError(name, `Cannot use reserved word "${name.text}" as an identifier`);
1✔
532
            }
533
            let operator = consume(
3,164✔
534
                `Expected operator ('=', '+=', '-=', '*=', '/=', '\\=', '^=', '<<=', or '>>=') after idenfifier '${name.text}'`,
535
                ...assignmentOperators
536
            );
537

538
            let value = expression();
3,164✔
539
            if (!check(...additionalterminators)) {
3,163✔
540
                consume(
3,134✔
541
                    "Expected newline or ':' after assignment",
542
                    Lexeme.Newline,
543
                    Lexeme.Colon,
544
                    Lexeme.Eof,
545
                    ...additionalterminators
546
                );
547
            }
548

549
            if (operator.kind === Lexeme.Equal) {
3,163✔
550
                return new Stmt.Assignment({ equals: operator }, name, value);
3,151✔
551
            } else {
552
                return new Stmt.Assignment(
12✔
553
                    { equals: operator },
554
                    name,
555
                    new Expr.Binary(new Expr.Variable(name), operator, value)
556
                );
557
            }
558
        }
559

560
        function checkLibrary() {
561
            let isLibraryIdentifier =
562
                check(Lexeme.Identifier) && peek().text.toLowerCase() === "library";
40,197✔
563
            //if we are at the top level, any line that starts with "library" should be considered a library statement
564
            if (isAtRootLevel() && isLibraryIdentifier) {
40,197✔
565
                return true;
6✔
566
            }
567
            //not at root level, library statements are all invalid here, but try to detect if the tokens look
568
            //like a library statement (and let the libraryStatement function handle emitting the errors)
569
            else if (isLibraryIdentifier && checkNext(Lexeme.String)) {
40,191✔
570
                return true;
1✔
571
            }
572
            //definitely not a library statement
573
            else {
574
                return false;
40,190✔
575
            }
576
        }
577

578
        function statement(...additionalterminators: BlockTerminator[]): Statement | undefined {
579
            if (checkLibrary()) {
18,522!
580
                return libraryStatement();
×
581
            }
582

583
            if (check(Lexeme.Stop)) {
18,522✔
584
                return stopStatement();
2✔
585
            }
586

587
            if (check(Lexeme.Try)) {
18,520✔
588
                return tryCatch();
8✔
589
            }
590

591
            if (check(Lexeme.If)) {
18,512✔
592
                return ifStatement();
247✔
593
            }
594

595
            if (check(Lexeme.Print)) {
18,265✔
596
                return printStatement(...additionalterminators);
9,528✔
597
            }
598

599
            if (check(Lexeme.While)) {
8,737✔
600
                return whileStatement();
13✔
601
            }
602

603
            if (check(Lexeme.ExitWhile)) {
8,724✔
604
                return exitWhile();
2✔
605
            }
606

607
            if (check(Lexeme.For)) {
8,722✔
608
                return forStatement();
18✔
609
            }
610

611
            if (check(Lexeme.ForEach)) {
8,704✔
612
                return forEachStatement();
66✔
613
            }
614

615
            if (check(Lexeme.ExitFor)) {
8,638✔
616
                return exitFor();
5✔
617
            }
618

619
            if (checkEnd()) {
8,633✔
620
                return endStatement();
1✔
621
            }
622

623
            if (match(Lexeme.Return)) {
8,632✔
624
                return returnStatement();
1,140✔
625
            }
626

627
            if (check(Lexeme.Dim)) {
7,492✔
628
                return dimStatement();
3✔
629
            }
630

631
            if (check(Lexeme.Goto)) {
7,489✔
632
                return gotoStatement();
3✔
633
            }
634

635
            //does this line look like a label? (i.e.  `someIdentifier:` )
636
            if (check(Lexeme.Identifier) && checkNext(Lexeme.Colon)) {
7,486✔
637
                return labelStatement();
2✔
638
            }
639

640
            // TODO: support multi-statements
641
            return setStatement(...additionalterminators);
7,484✔
642
        }
643

644
        function tryCatch(): Stmt.TryCatch {
645
            let tryKeyword = advance();
8✔
646
            let tryBlock = block(Lexeme.Catch);
8✔
647
            if (!tryBlock) {
8✔
648
                throw addError(peek(), "Expected 'catch' to terminate try block");
1✔
649
            }
650

651
            if (!check(Lexeme.Identifier)) {
7✔
652
                // defer this error so we can parse the `catch` block.
653
                // it'll be thrown if the catch block parses successfully otherwise.
654
                throw addError(peek(), "Expected variable name for caught error after 'catch'");
1✔
655
            }
656

657
            let caughtVariable = new Expr.Variable(advance() as Identifier);
6✔
658
            let catchBlock = block(Lexeme.EndTry);
6✔
659
            if (!catchBlock) {
6✔
660
                throw addError(peek(), "Expected 'end try' or 'endtry' to terminate catch block");
1✔
661
            }
662

663
            return new Stmt.TryCatch(tryBlock.body, catchBlock.body, caughtVariable, {
5✔
664
                try: tryKeyword,
665
                catch: tryBlock.closingToken,
666
                endtry: catchBlock.closingToken,
667
            });
668
        }
669

670
        function whileStatement(): Stmt.While {
671
            const whileKeyword = advance();
13✔
672
            const condition = expression();
13✔
673

674
            checkOrThrow(
13✔
675
                "Expected newline or ':' after 'while ...condition...'",
676
                Lexeme.Newline,
677
                Lexeme.Colon
678
            );
679
            const maybeWhileBlock = block(Lexeme.EndWhile);
13✔
680
            if (!maybeWhileBlock) {
13!
681
                throw addError(peek(), "Expected 'end while' to terminate while-loop block");
×
682
            }
683

684
            return new Stmt.While(
13✔
685
                { while: whileKeyword, endWhile: maybeWhileBlock.closingToken },
686
                condition,
687
                maybeWhileBlock.body
688
            );
689
        }
690

691
        function exitWhile(): Stmt.ExitWhile {
692
            let keyword = advance();
2✔
693
            checkOrThrow("Expected newline after 'exit while'", Lexeme.Newline);
2✔
694
            return new Stmt.ExitWhile({ exitWhile: keyword });
2✔
695
        }
696

697
        function forStatement(): Stmt.For {
698
            const forKeyword = advance();
18✔
699
            const initializer = assignment(Lexeme.To);
18✔
700
            const to = advance();
18✔
701
            const finalValue = expression();
18✔
702
            let increment: Expression | undefined;
703
            let step: Token | undefined;
704

705
            if (check(Lexeme.Step)) {
18✔
706
                step = advance();
6✔
707
                increment = expression();
6✔
708
            } else {
709
                // BrightScript for/to/step loops default to a step of 1 if no `step` is provided
710
                increment = new Expr.Literal(new Int32(1), peek().location);
12✔
711
            }
712

713
            let maybeBody = block(Lexeme.EndFor, Lexeme.Next);
18✔
714
            if (!maybeBody) {
18!
715
                throw addError(peek(), "Expected 'end for' or 'next' to terminate for-loop block");
×
716
            }
717

718
            // WARNING: BrightScript doesn't delete the loop initial value after a for/to loop! It just
719
            // stays around in scope with whatever value it was when the loop exited.
720
            return new Stmt.For(
18✔
721
                {
722
                    for: forKeyword,
723
                    to: to,
724
                    step: step,
725
                    endFor: maybeBody.closingToken,
726
                },
727
                initializer,
728
                finalValue,
729
                increment,
730
                maybeBody.body
731
            );
732
        }
733

734
        function forEachStatement(): Stmt.ForEach {
735
            let forEach = advance();
66✔
736
            let name = advance();
66✔
737

738
            let maybeIn = peek();
66✔
739
            if (check(Lexeme.Identifier) && maybeIn.text.toLowerCase() === "in") {
66!
740
                advance();
66✔
741
            } else {
742
                throw addError(maybeIn, "Expected 'in' after 'for each <name>'");
×
743
            }
744

745
            let target = expression();
66✔
746
            if (!target) {
66!
747
                throw addError(peek(), "Expected target object to iterate over");
×
748
            }
749
            advance();
66✔
750

751
            let maybeBody = block(Lexeme.EndFor, Lexeme.Next);
66✔
752
            if (!maybeBody) {
66!
753
                throw addError(peek(), "Expected 'end for' or 'next' to terminate for-loop block");
×
754
            }
755

756
            return new Stmt.ForEach(
66✔
757
                {
758
                    forEach: forEach,
759
                    in: maybeIn,
760
                    endFor: maybeBody.closingToken,
761
                },
762
                name,
763
                target,
764
                maybeBody.body
765
            );
766
        }
767

768
        function exitFor(): Stmt.ExitFor {
769
            let keyword = advance();
5✔
770
            checkOrThrow("Expected newline after 'exit for'", Lexeme.Newline);
5✔
771
            return new Stmt.ExitFor({ exitFor: keyword });
5✔
772
        }
773

774
        function libraryStatement(): Stmt.Library | undefined {
775
            let libraryStatement = new Stmt.Library({
7✔
776
                library: advance(),
777
                //grab the next token only if it's a string
778
                filePath: check(Lexeme.String) ? advance() : undefined,
7✔
779
            });
780

781
            //no token following library keyword token
782
            if (!libraryStatement.tokens.filePath && check(Lexeme.Newline, Lexeme.Colon)) {
7✔
783
                addErrorAtLocation(
1✔
784
                    libraryStatement.tokens.library.location,
785
                    `Missing string literal after ${libraryStatement.tokens.library.text} keyword`
786
                );
787
            }
788
            //does not have a string literal as next token
789
            else if (!libraryStatement.tokens.filePath && peek().kind === Lexeme.Newline) {
6!
790
                addErrorAtLocation(
×
791
                    peek().location,
792
                    `Expected string literal after ${libraryStatement.tokens.library.text} keyword`
793
                );
794
            }
795

796
            //consume all tokens until the end of the line
797
            let invalidTokens = consumeUntil(Lexeme.Newline, Lexeme.Eof, Lexeme.Colon);
7✔
798

799
            if (invalidTokens.length > 0) {
7✔
800
                //add an error for every invalid token
801
                for (let invalidToken of invalidTokens) {
1✔
802
                    addErrorAtLocation(
3✔
803
                        invalidToken.location,
804
                        `Found unexpected token '${invalidToken.text}' after library statement`
805
                    );
806
                }
807
            }
808

809
            //libraries must be at the very top of the file before any other declarations.
810
            let isAtTopOfFile = true;
7✔
811
            for (let statement of statements) {
7✔
812
                //if we found a non-library statement, this statement is not at the top of the file
813
                if (!(statement instanceof Stmt.Library)) {
2✔
814
                    isAtTopOfFile = false;
1✔
815
                }
816
            }
817

818
            //libraries must be a root-level statement (i.e. NOT nested inside of functions)
819
            if (!isAtRootLevel() || !isAtTopOfFile) {
7✔
820
                addErrorAtLocation(
2✔
821
                    libraryStatement.location,
822
                    "Library statements may only appear at the top of a file"
823
                );
824
            }
825
            //consume to the next newline, eof, or colon
826
            while (match(Lexeme.Newline, Lexeme.Eof, Lexeme.Colon));
7✔
827
            return libraryStatement;
7✔
828
        }
829

830
        function ifStatement(): Stmt.If {
831
            const ifToken = advance();
247✔
832
            const startingLine = ifToken.location;
247✔
833

834
            const condition = expression();
247✔
835
            let thenBranch: Stmt.Block;
836
            let elseIfBranches: Stmt.ElseIf[] = [];
245✔
837
            let elseBranch: Stmt.Block | undefined;
838

839
            let thenToken: Token | undefined;
840
            let elseIfTokens: Token[] = [];
245✔
841
            let endIfToken: Token | undefined;
842
            let elseToken: Token | undefined;
843

844
            /**
845
             * A simple wrapper around `check`, to make tests for a `then` identifier.
846
             * As with many other words, "then" is a keyword but not reserved, so associative
847
             * arrays can have properties called "then".  It's a valid identifier sometimes, so the
848
             * parser has to take on the burden of understanding that I guess.
849
             * @returns `true` if the next token is an identifier with text "then", otherwise `false`.
850
             */
851
            function checkThen() {
852
                return check(Lexeme.Identifier) && peek().text.toLowerCase() === "then";
261✔
853
            }
854

855
            if (checkThen()) {
245✔
856
                // `then` is optional after `if ...condition...`, so only advance to the next token if `then` is present
857
                thenToken = advance();
114✔
858
            }
859

860
            if (check(Lexeme.Newline) || check(Lexeme.Colon)) {
245✔
861
                //keep track of the current error count, because if the then branch fails,
862
                //we will trash them in favor of a single error on if
863
                let errorsLengthBeforeBlock = errors.length;
218✔
864

865
                // we're parsing a multi-line ("block") form of the BrightScript if/then/else and must find
866
                // a trailing "end if"
867

868
                let maybeThenBranch = block(Lexeme.EndIf, Lexeme.Else, Lexeme.ElseIf);
218✔
869
                if (!maybeThenBranch) {
218✔
870
                    //throw out any new errors created as a result of a `then` block parse failure.
871
                    //the block() function will discard the current line, so any discarded errors will
872
                    //resurface if they are legitimate, and not a result of a malformed if statement
873
                    errors.splice(errorsLengthBeforeBlock, errors.length - errorsLengthBeforeBlock);
2✔
874

875
                    //this whole if statement is bogus...add error to the if token and hard-fail
876
                    throw addError(
2✔
877
                        ifToken,
878
                        "Expected 'end if', 'else if', or 'else' to terminate 'then' block"
879
                    );
880
                }
881

882
                let blockEnd = maybeThenBranch.closingToken;
216✔
883
                if (blockEnd.kind === Lexeme.EndIf) {
216✔
884
                    endIfToken = blockEnd;
142✔
885
                }
886

887
                thenBranch = maybeThenBranch.body;
216✔
888

889
                // attempt to read a bunch of "else if" clauses
890
                while (blockEnd.kind === Lexeme.ElseIf) {
216✔
891
                    elseIfTokens.push(blockEnd);
13✔
892
                    let elseIfCondition = expression();
13✔
893
                    if (checkThen()) {
13✔
894
                        // `then` is optional after `else if ...condition...`, so only advance to the next token if `then` is present
895
                        advance();
8✔
896
                    }
897

898
                    let maybeElseIfThen = block(Lexeme.EndIf, Lexeme.Else, Lexeme.ElseIf);
13✔
899
                    if (!maybeElseIfThen) {
13!
900
                        throw addError(
×
901
                            peek(),
902
                            "Expected 'end if', 'else if', or 'else' to terminate 'then' block"
903
                        );
904
                    }
905

906
                    blockEnd = maybeElseIfThen.closingToken;
13✔
907
                    if (blockEnd.kind === Lexeme.EndIf) {
13✔
908
                        endIfToken = blockEnd;
1✔
909
                    }
910

911
                    elseIfBranches.push({
13✔
912
                        type: "ElseIf",
913
                        condition: elseIfCondition,
914
                        thenBranch: maybeElseIfThen.body,
915
                    });
916
                }
917

918
                if (blockEnd.kind === Lexeme.Else) {
216✔
919
                    elseToken = blockEnd;
73✔
920
                    let maybeElseBranch = block(Lexeme.EndIf);
73✔
921
                    if (!maybeElseBranch) {
73!
922
                        throw addError(peek(), "Expected 'end if' to terminate 'else' block");
×
923
                    }
924
                    elseBranch = maybeElseBranch.body;
73✔
925
                    endIfToken = maybeElseBranch.closingToken;
73✔
926

927
                    //ensure that single-line `if` statements have a colon right before 'end if'
928
                    if (ifToken.location.start.line === endIfToken.location.start.line) {
73✔
929
                        let index = tokens.indexOf(endIfToken);
2✔
930
                        let previousToken = tokens[index - 1];
2✔
931
                        if (previousToken.kind !== Lexeme.Colon) {
2✔
932
                            addError(endIfToken, "Expected ':' to preceed 'end if'");
1✔
933
                        }
934
                    }
935
                    match(Lexeme.Newline);
73✔
936
                } else {
937
                    if (!endIfToken) {
143!
938
                        throw addError(
×
939
                            blockEnd,
940
                            `Expected 'end if' to close 'if' statement started on line ${startingLine.start.line}`
941
                        );
942
                    }
943

944
                    //ensure that single-line `if` statements have a colon right before 'end if'
945
                    if (ifToken.location.start.line === endIfToken.location.start.line) {
143✔
946
                        let index = tokens.indexOf(endIfToken);
4✔
947
                        let previousToken = tokens[index - 1];
4✔
948
                        if (previousToken.kind !== Lexeme.Colon) {
4✔
949
                            addError(endIfToken, "Expected ':' to preceed 'end if'");
1✔
950
                        }
951
                    }
952
                    match(Lexeme.Newline);
143✔
953
                }
954
            } else {
955
                let maybeThenBranch = block(Lexeme.Newline, Lexeme.Eof, Lexeme.ElseIf, Lexeme.Else);
27✔
956
                if (!maybeThenBranch) {
27!
957
                    throw addError(
×
958
                        peek(),
959
                        "Expected a statement to follow 'if ...condition... then'"
960
                    );
961
                }
962
                thenBranch = maybeThenBranch.body;
27✔
963

964
                let closingToken = maybeThenBranch.closingToken;
27✔
965
                while (closingToken.kind === Lexeme.ElseIf) {
27✔
966
                    let elseIf = maybeThenBranch.closingToken;
3✔
967
                    elseIfTokens.push(elseIf);
3✔
968
                    let elseIfCondition = expression();
3✔
969
                    if (checkThen()) {
3✔
970
                        // `then` is optional after `else if ...condition...`, so only advance to the next token if `then` is present
971
                        advance();
2✔
972
                    }
973

974
                    let maybeElseIfBranch = block(
3✔
975
                        Lexeme.Newline,
976
                        Lexeme.Eof,
977
                        Lexeme.ElseIf,
978
                        Lexeme.Else
979
                    );
980
                    if (!maybeElseIfBranch) {
3!
981
                        throw addError(
×
982
                            peek(),
983
                            `Expected a statement to follow '${elseIf.text} ...condition... then'`
984
                        );
985
                    }
986
                    closingToken = maybeElseIfBranch.closingToken;
3✔
987

988
                    elseIfBranches.push({
3✔
989
                        type: "ElseIf",
990
                        condition: elseIfCondition,
991
                        thenBranch: maybeElseIfBranch.body,
992
                    });
993
                }
994

995
                if (
27✔
996
                    closingToken.kind !== Lexeme.Newline &&
34!
997
                    (closingToken.kind === Lexeme.Else || match(Lexeme.Else))
998
                ) {
999
                    elseToken = closingToken;
7✔
1000
                    let maybeElseBranch = block(Lexeme.Newline, Lexeme.Eof);
7✔
1001
                    if (!maybeElseBranch) {
7!
1002
                        throw addError(peek(), `Expected a statement to follow 'else'`);
×
1003
                    }
1004
                    elseBranch = maybeElseBranch.body;
7✔
1005
                }
1006
            }
1007

1008
            return new Stmt.If(
243✔
1009
                {
1010
                    if: ifToken,
1011
                    then: thenToken,
1012
                    elseIfs: elseIfTokens,
1013
                    endIf: endIfToken,
1014
                    else: elseToken,
1015
                },
1016
                condition,
1017
                thenBranch,
1018
                elseIfBranches,
1019
                elseBranch
1020
            );
1021
        }
1022

1023
        function setStatement(
1024
            ...additionalTerminators: BlockTerminator[]
1025
        ): Stmt.DottedSet | Stmt.IndexedSet | Stmt.Expression | Stmt.Increment {
1026
            /**
1027
             * Attempts to find an expression-statement or an increment statement.
1028
             * While calls are valid expressions _and_ statements, increment (e.g. `foo++`)
1029
             * statements aren't valid expressions. They _do_ however fall under the same parsing
1030
             * priority as standalone function calls though, so we cann parse them in the same way.
1031
             */
1032
            function _expressionStatement(): Stmt.Expression | Stmt.Increment {
1033
                let expressionStart = peek();
4,687✔
1034

1035
                if (check(Lexeme.PlusPlus, Lexeme.MinusMinus)) {
4,687✔
1036
                    let operator = advance();
18✔
1037

1038
                    if (check(Lexeme.PlusPlus, Lexeme.MinusMinus)) {
18✔
1039
                        throw addError(
1✔
1040
                            peek(),
1041
                            "Consecutive increment/decrement operators are not allowed"
1042
                        );
1043
                    } else if (expr instanceof Expr.Call) {
17✔
1044
                        throw addError(
1✔
1045
                            expressionStart,
1046
                            "Increment/decrement operators are not allowed on the result of a function call"
1047
                        );
1048
                    }
1049

1050
                    return new Stmt.Increment(expr, operator);
16✔
1051
                }
1052

1053
                if (!check(...additionalTerminators)) {
4,669✔
1054
                    consume(
4,668✔
1055
                        "Expected newline or ':' after expression statement",
1056
                        Lexeme.Newline,
1057
                        Lexeme.Colon,
1058
                        Lexeme.Eof
1059
                    );
1060
                }
1061

1062
                if (expr instanceof Expr.Call) {
4,669✔
1063
                    return new Stmt.Expression(expr);
4,666✔
1064
                }
1065

1066
                throw addError(
3✔
1067
                    expressionStart,
1068
                    "Expected statement or function call, but received an expression"
1069
                );
1070
            }
1071

1072
            let expr = call();
7,484✔
1073
            if (check(...assignmentOperators) && !(expr instanceof Expr.Call)) {
7,477✔
1074
                let left = expr;
2,790✔
1075
                let operator = advance();
2,790✔
1076
                let right = expression();
2,790✔
1077

1078
                // Create a dotted or indexed "set" based on the left-hand side's type
1079
                if (left instanceof Expr.IndexedGet) {
2,790✔
1080
                    consume(
66✔
1081
                        "Expected newline or ':' after indexed 'set' statement",
1082
                        Lexeme.Newline,
1083
                        Lexeme.Else,
1084
                        Lexeme.ElseIf,
1085
                        Lexeme.Colon,
1086
                        Lexeme.Eof
1087
                    );
1088

1089
                    return new Stmt.IndexedSet(
66✔
1090
                        left.obj,
1091
                        left.index,
1092
                        operator.kind === Lexeme.Equal
66✔
1093
                            ? right
1094
                            : new Expr.Binary(left, operator, right),
1095
                        left.closingSquare
1096
                    );
1097
                } else if (left instanceof Expr.DottedGet) {
2,724!
1098
                    consume(
2,724✔
1099
                        "Expected newline or ':' after dotted 'set' statement",
1100
                        Lexeme.Newline,
1101
                        Lexeme.Else,
1102
                        Lexeme.ElseIf,
1103
                        Lexeme.Colon,
1104
                        Lexeme.Eof
1105
                    );
1106

1107
                    return new Stmt.DottedSet(
2,724✔
1108
                        left.obj,
1109
                        left.name,
1110
                        operator.kind === Lexeme.Equal
2,724✔
1111
                            ? right
1112
                            : new Expr.Binary(left, operator, right)
1113
                    );
1114
                } else {
1115
                    return _expressionStatement();
×
1116
                }
1117
            } else {
1118
                return _expressionStatement();
4,687✔
1119
            }
1120
        }
1121

1122
        function printStatement(...additionalterminators: BlockTerminator[]): Stmt.Print {
1123
            let printKeyword = advance();
9,528✔
1124

1125
            let values: (Expr.Expression | Stmt.PrintSeparator.Tab | Stmt.PrintSeparator.Space)[] =
1126
                [];
9,528✔
1127

1128
            //print statements can be empty, so look for empty print conditions
1129
            if (isAtEnd() || check(Lexeme.Newline, Lexeme.Colon)) {
9,528✔
1130
                let emptyStringLiteral = new Expr.Literal(new BrsString(""), printKeyword.location);
1✔
1131
                values.push(emptyStringLiteral);
1✔
1132
            } else {
1133
                values.push(expression());
9,527✔
1134
            }
1135

1136
            while (!check(Lexeme.Newline, Lexeme.Colon, ...additionalterminators) && !isAtEnd()) {
9,528✔
1137
                if (check(Lexeme.Semicolon)) {
4,706✔
1138
                    values.push(advance() as Stmt.PrintSeparator.Space);
17✔
1139
                }
1140

1141
                if (check(Lexeme.Comma)) {
4,706✔
1142
                    values.push(advance() as Stmt.PrintSeparator.Tab);
3✔
1143
                }
1144

1145
                if (!check(Lexeme.Newline, Lexeme.Colon) && !isAtEnd()) {
4,706✔
1146
                    values.push(expression());
4,705✔
1147
                }
1148
            }
1149

1150
            if (!check(...additionalterminators)) {
9,528✔
1151
                consume(
9,519✔
1152
                    "Expected newline or ':' after printed values",
1153
                    Lexeme.Newline,
1154
                    Lexeme.Colon,
1155
                    Lexeme.Eof
1156
                );
1157
            }
1158

1159
            return new Stmt.Print({ print: printKeyword }, values);
9,528✔
1160
        }
1161

1162
        /**
1163
         * Parses a return statement with an optional return value.
1164
         * @returns an AST representation of a return statement.
1165
         */
1166
        function returnStatement(): Stmt.Return {
1167
            let tokens = { return: previous() };
1,140✔
1168

1169
            if (check(Lexeme.Colon, Lexeme.Newline, Lexeme.Eof)) {
1,140✔
1170
                while (match(Lexeme.Colon, Lexeme.Newline, Lexeme.Eof));
63✔
1171
                return new Stmt.Return(tokens);
63✔
1172
            }
1173

1174
            let toReturn = expression();
1,077✔
1175
            while (match(Lexeme.Newline, Lexeme.Colon));
1,077✔
1176

1177
            return new Stmt.Return(tokens, toReturn);
1,077✔
1178
        }
1179

1180
        /**
1181
         * Parses a `label` statement
1182
         * @returns an AST representation of an `label` statement.
1183
         */
1184
        function labelStatement() {
1185
            let tokens = {
2✔
1186
                identifier: advance(),
1187
                colon: advance(),
1188
            };
1189

1190
            consume("Labels must be declared on their own line", Lexeme.Newline, Lexeme.Eof);
2✔
1191

1192
            return new Stmt.Label(tokens);
2✔
1193
        }
1194

1195
        /**
1196
         * Parses a `dim` statement
1197
         * @returns an AST representation of an `goto` statement.
1198
         */
1199
        function dimStatement() {
1200
            let dimToken = advance();
3✔
1201

1202
            let name = consume("Expected variable name after 'dim'", Lexeme.Identifier);
3✔
1203

1204
            match(Lexeme.LeftSquare);
3✔
1205

1206
            let dimensions: Expression[] = [expression()];
3✔
1207
            while (!match(Lexeme.RightSquare)) {
2✔
1208
                consume("Expected ',' after expression in 'dim' statement", Lexeme.Comma);
1✔
1209
                dimensions.push(expression());
1✔
1210
            }
1211
            let rightSquare = previous();
2✔
1212

1213
            let tokens = {
2✔
1214
                dim: dimToken,
1215
                closingBrace: rightSquare,
1216
            };
1217

1218
            return new Stmt.Dim(tokens, name as Identifier, dimensions);
2✔
1219
        }
1220

1221
        /**
1222
         * Parses a `goto` statement
1223
         * @returns an AST representation of an `goto` statement.
1224
         */
1225
        function gotoStatement() {
1226
            let tokens = {
3✔
1227
                goto: advance(),
1228
                label: consume("Expected label identifier after goto keyword", Lexeme.Identifier),
1229
            };
1230

1231
            while (match(Lexeme.Newline, Lexeme.Colon));
3✔
1232

1233
            return new Stmt.Goto(tokens);
3✔
1234
        }
1235

1236
        /**
1237
         * Parses an `end` statement
1238
         * @returns an AST representation of an `end` statement.
1239
         */
1240
        function endStatement() {
1241
            let tokens = { end: advance() };
1✔
1242

1243
            while (match(Lexeme.Newline));
1✔
1244

1245
            return new Stmt.End(tokens);
1✔
1246
        }
1247
        /**
1248
         * Parses a `stop` statement
1249
         * @returns an AST representation of a `stop` statement
1250
         */
1251
        function stopStatement() {
1252
            let tokens = { stop: advance() };
2✔
1253

1254
            while (match(Lexeme.Newline, Lexeme.Colon));
2✔
1255

1256
            return new Stmt.Stop(tokens);
2✔
1257
        }
1258

1259
        /**
1260
         * Parses a block, looking for a specific terminating Lexeme to denote completion.
1261
         * @param terminators the token(s) that signifies the end of this block; all other terminators are
1262
         *                    ignored.
1263
         */
1264
        function block(
1265
            ...terminators: BlockTerminator[]
1266
        ): { body: Stmt.Block; closingToken: Token } | undefined {
1267
            let startingToken = peek();
4,237✔
1268

1269
            let statementSeparators = [Lexeme.Colon];
4,237✔
1270
            if (!terminators.includes(Lexeme.Newline)) {
4,237✔
1271
                statementSeparators.push(Lexeme.Newline);
4,200✔
1272
            }
1273

1274
            while (match(...statementSeparators));
4,237✔
1275

1276
            let closingToken: Token | undefined;
1277
            const statements: Statement[] = [];
4,237✔
1278
            while (!check(...terminators) && !isAtEnd()) {
4,237✔
1279
                //grab the location of the current token
1280
                let loopCurrent = current;
21,312✔
1281
                let dec = declaration(...terminators);
21,312✔
1282

1283
                if (dec) {
21,312✔
1284
                    statements.push(dec);
21,305✔
1285
                } else {
1286
                    //something went wrong. reset to the top of the loop
1287
                    current = loopCurrent;
7✔
1288

1289
                    //scrap the entire line
1290
                    consumeUntil(Lexeme.Colon, Lexeme.Newline, Lexeme.Eof);
7✔
1291
                    //trash the newline character so we start the next iteraion on the next line
1292
                    advance();
7✔
1293
                }
1294

1295
                if (checkPrevious(...terminators)) {
21,312✔
1296
                    closingToken = previous();
13✔
1297
                    while (match(...statementSeparators));
13✔
1298
                    break;
13✔
1299
                } else {
1300
                    while (match(...statementSeparators));
21,299✔
1301
                }
1302
            }
1303

1304
            if (isAtEnd() && !terminators.includes(Lexeme.Eof)) {
4,237✔
1305
                return undefined;
4✔
1306
                // TODO: Figure out how to handle unterminated blocks well
1307
            }
1308

1309
            // consume the last terminator
1310
            if (check(...terminators) && !closingToken) {
4,233✔
1311
                closingToken = advance();
4,220✔
1312
            }
1313

1314
            if (!closingToken) {
4,233!
1315
                return undefined;
×
1316
            }
1317

1318
            //the block's location starts at the end of the preceeding token, and stops at the beginning of the `end` token
1319
            const location: Location = {
4,233✔
1320
                file: startingToken.location.file,
1321
                start: startingToken.location.start,
1322
                end: closingToken.location.start,
1323
            };
1324

1325
            return {
4,233✔
1326
                body: new Stmt.Block(statements, location),
1327
                closingToken,
1328
            };
1329
        }
1330

1331
        function expression(): Expression {
1332
            return anonymousFunction();
38,172✔
1333
        }
1334

1335
        function anonymousFunction(): Expression {
1336
            if (check(Lexeme.Sub, Lexeme.Function)) {
38,172✔
1337
                return functionDeclaration(true);
234✔
1338
            }
1339

1340
            return boolean();
37,938✔
1341
        }
1342

1343
        function boolean(): Expression {
1344
            let expr = relational();
37,938✔
1345

1346
            while (match(Lexeme.And, Lexeme.Or)) {
37,933✔
1347
                let operator = previous();
21✔
1348
                let right = relational();
21✔
1349
                expr = new Expr.Binary(expr, operator, right);
21✔
1350
            }
1351

1352
            return expr;
37,933✔
1353
        }
1354

1355
        function relational(): Expression {
1356
            let expr = bitshift();
38,039✔
1357

1358
            while (
38,034✔
1359
                match(
1360
                    Lexeme.Equal,
1361
                    Lexeme.LessGreater,
1362
                    Lexeme.Greater,
1363
                    Lexeme.GreaterEqual,
1364
                    Lexeme.Less,
1365
                    Lexeme.LessEqual
1366
                )
1367
            ) {
1368
                let operator = previous();
500✔
1369
                let right = bitshift();
500✔
1370
                expr = new Expr.Binary(expr, operator, right);
500✔
1371
            }
1372

1373
            return expr;
38,034✔
1374
        }
1375

1376
        function bitshift(): Expression {
1377
            let expr = additive();
38,539✔
1378

1379
            while (match(Lexeme.LeftShift, Lexeme.RightShift)) {
38,534✔
1380
                let operator = previous();
6✔
1381
                let right = additive();
6✔
1382
                expr = new Expr.Binary(expr, operator, right);
6✔
1383
            }
1384

1385
            return expr;
38,534✔
1386
        }
1387

1388
        function additive(): Expression {
1389
            let expr = multiplicative();
38,545✔
1390

1391
            while (match(Lexeme.Plus, Lexeme.Minus)) {
38,540✔
1392
                let operator = previous();
639✔
1393
                let right = multiplicative();
639✔
1394
                expr = new Expr.Binary(expr, operator, right);
639✔
1395
            }
1396

1397
            return expr;
38,540✔
1398
        }
1399

1400
        function multiplicative(): Expression {
1401
            let expr = exponential();
39,184✔
1402

1403
            while (match(Lexeme.Slash, Lexeme.Backslash, Lexeme.Star, Lexeme.Mod)) {
39,179✔
1404
                let operator = previous();
27✔
1405
                let right = exponential();
27✔
1406
                expr = new Expr.Binary(expr, operator, right);
27✔
1407
            }
1408

1409
            return expr;
39,179✔
1410
        }
1411

1412
        function exponential(): Expression {
1413
            let expr = prefixUnary();
39,211✔
1414

1415
            while (match(Lexeme.Caret)) {
39,206✔
1416
                let operator = previous();
9✔
1417
                let right = prefixUnary();
9✔
1418
                expr = new Expr.Binary(expr, operator, right);
9✔
1419
            }
1420

1421
            return expr;
39,206✔
1422
        }
1423

1424
        function prefixUnary(): Expression {
1425
            if (match(Lexeme.Not)) {
39,376✔
1426
                let operator = previous();
80✔
1427
                let right = relational();
80✔
1428
                return new Expr.Unary(operator, right);
80✔
1429
            } else if (match(Lexeme.Minus, Lexeme.Plus)) {
39,296✔
1430
                let operator = previous();
156✔
1431
                let right = prefixUnary();
156✔
1432
                return new Expr.Unary(operator, right);
156✔
1433
            }
1434

1435
            return call();
39,140✔
1436
        }
1437

1438
        function call(): Expression {
1439
            let expr = primary();
46,624✔
1440

1441
            function indexedGet() {
1442
                while (match(Lexeme.Newline));
420✔
1443

1444
                let index = expression();
420✔
1445

1446
                while (match(Lexeme.Newline));
420✔
1447
                let closingSquare = consume(
420✔
1448
                    "Expected ']' after array or object index",
1449
                    Lexeme.RightSquare
1450
                );
1451

1452
                expr = new Expr.IndexedGet(expr, index, closingSquare);
420✔
1453
            }
1454

1455
            function dottedGet() {}
1456

1457
            while (true) {
46,614✔
1458
                if (match(Lexeme.LeftParen)) {
79,467✔
1459
                    expr = finishCall(expr);
13,068✔
1460
                } else if (match(Lexeme.LeftSquare)) {
66,399✔
1461
                    indexedGet();
417✔
1462
                } else if (match(Lexeme.Dot)) {
65,982✔
1463
                    if (match(Lexeme.LeftSquare)) {
19,370✔
1464
                        indexedGet();
3✔
1465
                    } else {
1466
                        while (match(Lexeme.Newline));
19,367✔
1467

1468
                        let name = consume(
19,367✔
1469
                            "Expected property name after '.'",
1470
                            Lexeme.Identifier,
1471
                            ...allowedProperties
1472
                        );
1473

1474
                        // force it into an identifier so the AST makes some sense
1475
                        name.kind = Lexeme.Identifier;
19,366✔
1476

1477
                        expr = new Expr.DottedGet(expr, name as Identifier);
19,366✔
1478
                    }
1479
                } else {
1480
                    break;
46,612✔
1481
                }
1482
            }
1483

1484
            return expr;
46,612✔
1485
        }
1486

1487
        function finishCall(callee: Expression): Expression {
1488
            let args = [];
13,068✔
1489
            while (match(Lexeme.Newline));
13,068✔
1490

1491
            if (!check(Lexeme.RightParen)) {
13,068✔
1492
                do {
8,945✔
1493
                    while (match(Lexeme.Newline));
12,930✔
1494

1495
                    if (args.length >= Expr.Call.MaximumArguments) {
12,930!
1496
                        throw addError(
×
1497
                            peek(),
1498
                            `Cannot have more than ${Expr.Call.MaximumArguments} arguments`
1499
                        );
1500
                    }
1501
                    args.push(expression());
12,930✔
1502
                } while (match(Lexeme.Comma));
1503
            }
1504

1505
            while (match(Lexeme.Newline));
13,067✔
1506
            const closingParen = consume(
13,067✔
1507
                "Expected ')' after function call arguments",
1508
                Lexeme.RightParen
1509
            );
1510

1511
            return new Expr.Call(callee, closingParen, args);
13,067✔
1512
        }
1513

1514
        function primary(): Expression {
1515
            switch (true) {
46,624!
1516
                case match(Lexeme.False):
1517
                    return new Expr.Literal(BrsBoolean.False, previous().location);
933✔
1518
                case match(Lexeme.True):
1519
                    return new Expr.Literal(BrsBoolean.True, previous().location);
1,111✔
1520
                case match(Lexeme.Invalid):
1521
                    return new Expr.Literal(BrsInvalid.Instance, previous().location);
139✔
1522
                case match(
1523
                    Lexeme.Integer,
1524
                    Lexeme.LongInteger,
1525
                    Lexeme.Float,
1526
                    Lexeme.Double,
1527
                    Lexeme.String
1528
                ):
1529
                    return new Expr.Literal(previous().literal!, previous().location);
20,715✔
1530
                case match(Lexeme.Identifier):
1531
                    return new Expr.Variable(previous() as Identifier);
21,859✔
1532
                case match(Lexeme.LeftParen):
1533
                    let left = previous();
179✔
1534
                    let expr = expression();
179✔
1535
                    let right = consume(
179✔
1536
                        "Unmatched '(' - expected ')' after expression",
1537
                        Lexeme.RightParen
1538
                    );
1539
                    return new Expr.Grouping({ left, right }, expr);
179✔
1540
                case match(Lexeme.LeftSquare):
1541
                    let elements: Expression[] = [];
534✔
1542
                    let openingSquare = previous();
534✔
1543

1544
                    while (match(Lexeme.Newline));
534✔
1545

1546
                    if (!match(Lexeme.RightSquare)) {
534✔
1547
                        elements.push(expression());
529✔
1548

1549
                        while (match(Lexeme.Comma, Lexeme.Newline)) {
529✔
1550
                            while (match(Lexeme.Newline));
838✔
1551

1552
                            if (check(Lexeme.RightSquare)) {
838✔
1553
                                break;
3✔
1554
                            }
1555

1556
                            elements.push(expression());
835✔
1557
                        }
1558

1559
                        consume(
529✔
1560
                            "Unmatched '[' - expected ']' after array literal",
1561
                            Lexeme.RightSquare
1562
                        );
1563
                    }
1564

1565
                    let closingSquare = previous();
534✔
1566

1567
                    //consume("Expected newline or ':' after array literal", Lexeme.Newline, Lexeme.Colon, Lexeme.Eof);
1568
                    return new Expr.ArrayLiteral(elements, openingSquare, closingSquare);
534✔
1569
                case match(Lexeme.LeftBrace):
1570
                    let openingBrace = previous();
1,144✔
1571
                    let members: Expr.AAMember[] = [];
1,144✔
1572

1573
                    function key() {
1574
                        let k;
1575
                        if (check(Lexeme.Identifier, ...allowedProperties)) {
1,566✔
1576
                            k = new BrsString(advance().text!);
1,488✔
1577
                        } else if (check(Lexeme.String)) {
78!
1578
                            k = advance().literal! as BrsString;
78✔
1579
                        } else {
1580
                            throw addError(
×
1581
                                peek(),
1582
                                `Expected identifier or string as associative array key, but received '${
1583
                                    peek().text || ""
×
1584
                                }'`
1585
                            );
1586
                        }
1587

1588
                        consume(
1,566✔
1589
                            "Expected ':' between associative array key and value",
1590
                            Lexeme.Colon
1591
                        );
1592
                        return k;
1,566✔
1593
                    }
1594

1595
                    while (match(Lexeme.Newline));
1,144✔
1596

1597
                    if (!match(Lexeme.RightBrace)) {
1,144✔
1598
                        members.push({
907✔
1599
                            name: key(),
1600
                            value: expression(),
1601
                        });
1602

1603
                        while (match(Lexeme.Comma, Lexeme.Newline, Lexeme.Colon)) {
907✔
1604
                            while (match(Lexeme.Newline, Lexeme.Colon));
1,153✔
1605

1606
                            if (check(Lexeme.RightBrace)) {
1,153✔
1607
                                break;
494✔
1608
                            }
1609

1610
                            members.push({
659✔
1611
                                name: key(),
1612
                                value: expression(),
1613
                            });
1614
                        }
1615

1616
                        consume(
907✔
1617
                            "Unmatched '{' - expected '}' after associative array literal",
1618
                            Lexeme.RightBrace
1619
                        );
1620
                    }
1621

1622
                    let closingBrace = previous();
1,144✔
1623

1624
                    return new Expr.AALiteral(members, openingBrace, closingBrace);
1,144✔
1625
                case match(Lexeme.Pos, Lexeme.Tab):
1626
                    let token = Object.assign(previous(), {
×
1627
                        kind: Lexeme.Identifier,
1628
                    }) as Identifier;
1629
                    return new Expr.Variable(token);
×
1630
                case check(Lexeme.Function, Lexeme.Sub):
1631
                    return anonymousFunction();
×
1632
                default:
1633
                    throw addError(peek(), `Found unexpected token '${peek().text}'`);
10✔
1634
            }
1635
        }
1636

1637
        function match(...lexemes: Lexeme[]) {
1638
            for (let lexeme of lexemes) {
935,135✔
1639
                if (check(lexeme)) {
1,650,670✔
1640
                    advance();
98,918✔
1641
                    return true;
98,918✔
1642
                }
1643
            }
1644

1645
            return false;
836,217✔
1646
        }
1647

1648
        /**
1649
         * Consume tokens until one of the `stopLexemes` is encountered
1650
         * @param lexemes
1651
         * @return - the list of tokens consumed, EXCLUDING the `stopLexeme` (you can use `peek()` to see which one it was)
1652
         */
1653
        function consumeUntil(...stopLexemes: Lexeme[]) {
1654
            let result = [] as Token[];
14✔
1655
            //take tokens until we encounter one of the stopLexemes
1656
            while (!stopLexemes.includes(peek().kind)) {
14✔
1657
                result.push(advance());
12✔
1658
            }
1659
            return result;
14✔
1660
        }
1661

1662
        /**
1663
         * Checks that the next token is one of a list of lexemes and returns that token, and *advances past it*.
1664
         * If the next token is none of the provided lexemes, throws an error.
1665
         * @param message - the message to include in the thrown error if the next token isn't one of the provided `lexemes`
1666
         * @param lexemes - the set of `lexemes` to check for
1667
         *
1668
         * @see checkOrError
1669
         */
1670
        function consume(message: string, ...lexemes: Lexeme[]): Token {
1671
            let foundLexeme = lexemes
66,655✔
1672
                .map((lexeme) => peek().kind === lexeme)
973,484✔
1673
                .reduce((foundAny, foundCurrent) => foundAny || foundCurrent, false);
973,484✔
1674

1675
            if (foundLexeme) {
66,655✔
1676
                return advance();
66,654✔
1677
            }
1678
            throw addError(peek(), message);
1✔
1679
        }
1680

1681
        function advance(): Token {
1682
            if (!isAtEnd()) {
200,230✔
1683
                current++;
200,112✔
1684
            }
1685
            return previous();
200,230✔
1686
        }
1687

1688
        /**
1689
         * Checks that the next token is one of a list of lexemes and returns that token, but *does not advance past it*.
1690
         * If the next token is none of the provided lexemes, throws an error.
1691
         * @param message - the message to include in the thrown error if the next token isn't one of the provided `lexemes`
1692
         * @param lexemes - the set of `lexemes` to check for
1693
         *
1694
         * @see consume
1695
         */
1696
        function checkOrThrow(message: string, ...lexemes: Lexeme[]): Token {
1697
            let foundLexeme = lexemes
3,805✔
1698
                .map((lexeme) => peek().kind === lexeme)
7,603✔
1699
                .reduce((foundAny, foundCurrent) => foundAny || foundCurrent, false);
7,603✔
1700
            if (foundLexeme) {
3,805✔
1701
                return peek();
3,805✔
1702
            }
1703

1704
            throw addError(peek(), message);
×
1705
        }
1706

1707
        /**
1708
         * Check that the previous token matches one of the specified Lexemes
1709
         * @param lexemes
1710
         */
1711
        function checkPrevious(...lexemes: Lexeme[]) {
1712
            if (current === 0) {
21,312!
1713
                return false;
×
1714
            } else {
1715
                current--;
21,312✔
1716
                var result = check(...lexemes);
21,312✔
1717
                current++;
21,312✔
1718
                return result;
21,312✔
1719
            }
1720
        }
1721

1722
        function check(...lexemes: Lexeme[]) {
1723
            if (isAtEnd()) {
2,054,483✔
1724
                return false;
6,695✔
1725
            }
1726

1727
            return lexemes.some((lexeme) => peek().kind === lexeme);
2,277,436✔
1728
        }
1729

1730
        function checkNext(...lexemes: Lexeme[]) {
1731
            if (isAtEnd()) {
18,187!
1732
                return false;
×
1733
            }
1734

1735
            return lexemes.some((lexeme) => peekNext().kind === lexeme);
71,143✔
1736
        }
1737

1738
        function isAtEnd() {
1739
            return peek().kind === Lexeme.Eof;
2,419,390✔
1740
        }
1741

1742
        function peekNext() {
1743
            if (isAtEnd()) {
71,143!
1744
                return peek();
×
1745
            }
1746
            return tokens[current + 1];
71,143✔
1747
        }
1748

1749
        function peek() {
1750
            return tokens[current];
5,725,519✔
1751
        }
1752

1753
        function previous() {
1754
            return tokens[current - 1];
271,873✔
1755
        }
1756

1757
        function synchronize() {
1758
            advance(); // skip the erroneous token
21✔
1759

1760
            while (!isAtEnd()) {
21✔
1761
                if (previous().kind === Lexeme.Newline || previous().kind === Lexeme.Colon) {
23✔
1762
                    // newlines and ':' characters separate statements
1763
                    return;
3✔
1764
                }
1765

1766
                switch (peek().kind) {
20!
1767
                    case Lexeme.Function:
1768
                    case Lexeme.Sub:
1769
                    case Lexeme.If:
1770
                    case Lexeme.For:
1771
                    case Lexeme.ForEach:
1772
                    case Lexeme.While:
1773
                    case Lexeme.Print:
1774
                    case Lexeme.Return:
1775
                        // start parsing again from the next block starter or obvious
1776
                        // expression start
1777
                        return;
×
1778
                }
1779

1780
                advance();
20✔
1781
            }
1782
        }
1783
    }
1784
}
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